Kang Su Gatlin
Microsoft Corporation
February 2003
日本語版最終更新日 2003 年 9 月 30 日
適用対象
Microsoft® Visual C++®
Microsoft Windows® XP アプリケーション開発
Microsoft Windows Server 2003 アプリケーション開発
要約: データの整列の問題に取り組むために必要な情報を開発者に提供します。データの整列は、Microsoft Windows XP および Microsoft Windows Server 2003 プラットフォーム用に開発された 64 ビットおよび 32 ビット アプリケーションのパフォーマンス向上には重要です。
目次
はじめに
データの整列とは
整列の考慮が必要な理由
データ整列の例外と修正
コンパイラでの整列のサポート
整列の問題の回避方法に関するクイック ヒント
命令の整列について
まとめ
はじめに
インテル® および AMD® は、インテル Itanium® Processor Family (IPF) アーキテクチャおよび AMD x86-64 アーキテクチャという新しいプロセッサ ファミリを発表しました。これらのプロセッサは、Microsoft Windows デスクトップ/サーバーの世界に IA-32 インテル アーキテクチャ ファミリを結合しています。これらのプラットフォームで Microsoft Visual C++ と Microsoft Windows を使用するとパフォーマンスが著しく向上しますが、この優れたパフォーマンスは特定のプログラミング手順を条件としています。これらのプログラミング手順の 1 つは、適切なデータの整列です。適切なデータの整列により、64 ビットおよび 32 ビット アプリケーションを最大限に活用できます。また、Itanium では、パフォーマンスの問題だけでなく正確さの問題にも影響する場合があります。
この記事では、データの整列に注意する必要がある理由、データの整列を行わない場合の損失、データを整列する方法、およびデータを整列できない場合にどうすればよいかを説明します。データ アクセスが同じ方法で再び行われることはありません。
データの整列とは
すべての変数には、1) 値および 2) 格納場所という 2 つのコンポーネントが関連付けられています。ここでは、格納場所について見ていきます。変数の格納場所は "アドレス" とも呼ばれ、メモリ内のデータが開始する整数 (データ型ではなく数学用語での整数) オフセットです。特定の変数の整列は 2 の累乗の最大値 L であり、変数のアドレスを A とすると、この 2 の累乗値の剰余は 0 です。つまり、A mod L = 0 です。この変数は、L バイト整列と呼ばれます。X > Y で、X と Y の両方が 2 の累乗値である場合は、X バイト整列の変数は Y バイト整列でもあることに注意してください。
リスト 1 のサンプル コードでは、変数が格納/整列される場所を示します。整列が行われている理由がわからなくても気にしないでください。この記事の終わりまでに、すべて理解できます。サンプルを楽しんで試してみることをお勧めします (ローカル変数とクラス メンバ変数を並べ替えて、アドレスがどのようになるかを確認してください)。
リスト 1. データの整列の例
#include <stdio.h>
int main()
{
char a;
char b;
class S1
{
public:
char m_1; // 1 バイトの要素。
// 3 バイトのパディングがここに配置されます。
int m_2; // 4 バイトの要素。
double m_3, m_4; // 8 バイトの要素。
};
S1 x;
long y;
S1 z[5];
printf("b = %p\n", &b);
printf("x = %p\n", &x);
printf("x.m_2 = %p\n", &x.m_2);
printf("x.m_3 = %p\n", &x.m_3);
printf("y = %p\n", &y);
printf("z[0] = %p\n", z);
printf("z[1] = %p\n", &z[1]);
return 0;
}
リスト 2 では、リスト 1 の結果がどのように出力されるかを示します。これは、筆者のコンピュータで出力される内容です。読者のコンピュータでは、おそらく異なる数値が出力されますが、 気にしないでください。
リスト 2. リスト 1 の例の出力
b = 000006FBFFB8FEB1
x = 000006FBFFB8FE98
x.m_2 = 000006FBFFB8FE9C
x.m_3 = 000006FBFFB8FEA0
y = 000006FBFFB8FE90
z[0] = 000006FBFFB8FEB8
z[1] = 000006FBFFB8FED0
このリスト 1 と 2 の例から、各変数がどのように整列されるかがわかります。文字 b は、1 バイト境界に整列されます (0xB1 % 2 = 1)。クラス x は、8 バイト境界に整列されます (0x98 % 8 = 0)。メンバ x.m_2 は 4 バイト境界に整列されます (0x9C % 8 = 4)。x.m_3 は、y と同様に 8 バイト境界にあります。z[0] と z[1] も 8 バイト整列です (最後の変数セットの剰余計算は単純であるため省略します)。
クラス S1 を見ると、クラス全体が 8 バイト整列になっていることがわかります。要素 x.m_1 と x.m_2 の間に 4 バイトのギャップが存在しますが、x.m_1 は 1 バイトの要素であるため、クラス内のパッキングは最適ではありません。
Itanium および x86-64 コンパイラは、自然長が 1、2、4、8、10、および 16 バイトであるデータ アイテムを備えています。長さが 8 バイトを超えるアイテムを除き、すべての型は自然長に整列されます。8 バイトを超えるアイテムは、次の 2 の累乗境界に整列されます。たとえば、10 バイトのデータ アイテムは、16 バイト境界に整列されます。x86 コンパイラでは、1、2、4、および 8 バイトの自然長の境界への整列がサポートされます。
次に、特定の型の整列を決定する比較的簡単な方法を示します。これを行うには、__alignof(type) 演算子を使用します (同等のマクロは TYPE_ALIGNMENT(type) です)。この演算子は、渡された変数/型の整列要件を返します。
スタックの整列
どちらの 64 ビット プラットフォームでも、スタックは 16 バイト整列です。これにより、必要な量よりも多くの領域が使用されますが、すべての要素が整列されるような方法でコンパイラがすべてのデータをスタックに配置できます。
x86 コンパイラは、スタックの整列に別の方法を使用します。既定では、スタックは 4 バイト整列です。この方法では領域の効率がよくなりますが、8 バイト整列を必要とするデータ型もあり、パフォーマンスを向上させるには 16 バイト整列が必要な場合があります。コンパイラは、動的 8 バイト スタック整列が有利であると判断することもあります。スタックに double 値がある場合は特にこの傾向が高くなります。
コンパイラは、この処理を 2 とおりの方法で行います。最初の方法では、コンパイラは、コンパイル時およびリンク時にユーザーによって指定された場合に、リンク時のコード生成 (LTCG) を使用してプログラム全体のコール ツリーを生成できます。これにより、コンパイラは 8 バイト スタック整列が有利となるコール ツリーの領域を判断でき、動的スタック整列が最も効果的なコール サイトを判断します。2 番目の方法は、関数がスタック上に double 値を持ち、何らかの理由で 8 バイト整列になっていない場合に使用されます。コンパイラは、ヒューリスティック (コンパイラを反復するたびに改善されます) を適用して、関数を動的に 8 バイト整列にする必要があるかどうかを判断します。
注: 動的 8 バイト スタック整列のパフォーマンス上の欠点は、フレーム ポインタの省略 (/Oy) が事実上オフになることです。動的 8 バイト スタックでスタックを参照するにはレジスタ EBP を使用する必要があるので、このレジスタを関数で一般レジスタとして使用することはできません。
構造体と共用体のレイアウト
構造体と共用体での整列に関するレイアウトは、少数の単純なルールによって決まります。構造体と共用体の整列は、構造体/共用体間整列および構造体内整列という 2 つのコンポーネントに分類できます (共用体内整列はありません)。
構造体/共用体間整列の方が単純です。この場合のルールは、コンパイラが、任意の構造体メンバの最大整列要件で構造体を整列するということです。共用体は、共用体が最初の共用体メンバ (辞書順) の整列要件に基づいて整列されるというルールに従います。
構造体内整列は、メンバがコンパイラにより自然境界で整列されるという原理に基づいて動作し、パディング制限に達するまで必要な量のパディングを挿入することで整列を行います。パディング制限は、コンパイル スイッチ /Zpn で設定されます。このスイッチの既定は /Zp8 です。
プログラマは、構造体の宣言のポイントで #pragma pack を使用して、そのポイントから前方の変換単位にパディング制限を設定することもできます。つまり、#pragma pack よりも前に宣言されている構造体には影響しません。パックされている構造体メンバにアクセスすると、整列されていないデータがアクセスされる場合があります。コンパイラは、これらのメンバの修正コードを挿入します。このため、アクセスによる例外は発生しなくなりますが、コードの速度が低下し、コード量が増加します (修正コードや例外についてまだ理解できなくても、この記事の終わりまでに理解できるようになります)。
パディング制限 (#pragma pack および /Zpn) は、注意して使用する必要があります。作業内容のほとんどが特定の要素の読み取りや書き込みを行わない単純なデータ移動で構成される場合、または領域が制約されている場合を除き、整列ルールに違反するパディング制限を使用した場合のトレードオフは、通常はプログラマに有利には働きません。
整列の考慮が必要な理由
これで、変数の整列の意味がわかりました。整列について注意する必要があるのはなぜでしょうか。もうおわかりでしょう。その理由はパフォーマンスです。また、Itanium プラットフォームでは、不整列の処理方法により生じる正確さも理由になります。問題は、その理由です。整列に注意する基本的な理由は何でしょうか。コンピュータ設計者が問題を難しくするために決定したわけではありません。これらの整列の問題は、実際には、コンピュータ設計者が決定したアーキテクチャ上のトレードオフの名残です。
最新の RISC ベース デザインでは、データは、要求されるデータの自然長で定義されている境界でだけアクセスできます。宛先レジスタには、その長さのデータが充填されます。このため、コンピュータは、自然長の積であるアドレスからデータを自然長のチャンクで取得します。したがって、自然長の積でないアドレスからデータを読み取ることには問題があります (アプリケーションの速度が低下したりクラッシュしたりすることがあります)。
たとえば、0 から始まるワード境界を持つ 32 ビット コンピュータは、1 回の読み込みで 0 〜 3、4 〜 7、または 40 〜 43 の位置にあるバイトからデータを読み込むことができますが、2 〜 5 の位置にあるデータを 1 回で読み込むことはできません (バイト 2 〜 5 は 2 ワードにまたがるからです)。このため、コンピュータが実際に 2 〜 5 の位置から 32 ビット値を取得する必要がある場合は、0 〜 3 からデータを取得し、4 〜 7 の位置からも値を取得する必要があります。さらに、必要なバイトを適切に抽出してシフトする操作を実行する必要があります。コンピュータ システムに応じて、オペレーティング システムまたはコンパイラがこの処理を行います。オペレーティング システムまたはコンパイラが行わない場合は、ハードウェアで例外が発生します (例外が発生するのは好ましくなく、さらに悪い場合はクラッシュすることもあります)。ソフトウェアが救済を行う場合は、余分なロジックだけでなく、余分なメモリ アクセスも発生します。実際に、最近のコンピュータの多くのアプリケーションでは、メモリ システムがパフォーマンスのボトルネックになっているので、余分なメモリ要求のコストは非常に高くなることがあります。この段落の例では、整列されたアドレスから 32 ビット値を取得するために 1 回のメモリ アクセスが行われるのではなく、2 〜 5 の 32 ビット値にアクセスするために 2 回のメモリ アクセスが行われます。このわかりにくいトピックをよく理解するには、図 1 を参照してください。

図 1. アドレス 2 〜 5 にあるバイトの読み込みを示した図
図 1 は、a) 1 回目のワードの読み込み (バイト 0 〜 3)、b) 読み込んだワードからのバイト 2 〜 3 の抽出、c) 2 回目のワードの読み込み、d) 2 回目に読み込んだワードからの最初の 2 バイトの抽出および前に抽出したバイトへの追加、を示しています。
このデータの整列の概念は、特定のコンピュータ アーキテクチャのワード サイズ以外に、複数レベルのキャッシュを通じたメモリ階層、変換参照バッファ、およびページにも拡張されます。32 ビット ワードと同様に、これらにも単位チャンク サイズが関連付けられています。キャッシュには、32 〜 128 バイト程度のキャッシュ ラインがあります。ページのサイズは 1024 バイト〜数メガバイトです。これはすべて、プログラムをより効率的に実行するために行われます。その処理方法については、問題になったときにだけ知る必要があります。
データの整列の例外と修正
整列の問題に対処する明白な方法はそれを回避することですが、現実には常に回避できるとは限りません。正しいプログラムの生成を支援するために、Microsoft Visual C++ と Microsoft Windows には、プログラマに役立つメカニズムが用意されています。これらのメカニズムはパフォーマンスにある程度影響しますが、アプリケーションの迅速な開発や移植を支援します。
考慮する必要のある最初の問題は、"整列の制限に違反するとどうなるか" ということです。つまり、整列エラーが発生するとどうなるかということです。いくつかの問題が発生し、それはどれも望ましいものではありません。
Windows では、整列エラーを生成するアプリケーション プログラムで、例外 EXCEPTION_DATATYPE_MISALIGNMENT が発生します。Itanium では、既定でオペレーティング システム (OS) がこの例外をアプリケーションから参照できるようにするので、この場合は終了ハンドラが役立つことがあります。ハンドラをセットアップしないと、プログラムがハングまたはクラッシュします。リスト 3 に、EXCEPTION_DATATYPE_MISALIGNMENT 例外をキャッチする方法を示します。
リスト 3. Itanium で整列の例外をキャッチするコード
#include <windows.h>
#include <stdio.h>
int mswindows_handle_hardware_exceptions (DWORD code)
{
printf("例外処理\n");
if (code == STATUS_DATATYPE_MISALIGNMENT)
{
printf("不整列エラー\n");
return EXCEPTION_EXECUTE_HANDLER;
}
else
return EXCEPTION_CONTINUE_SEARCH;
}
int main()
{
__try {
char temp[10];
memset(temp, 0, 10);
double *val;
val = (double *)(&temp[3]);
printf("%lf\n", *val);
}
__except(mswindows_handle_hardware_exceptions (GetExceptionCode ())) {}
}
アプリケーションは、整列エラーの動作を、既定の動作から、整列エラーが修正された動作に変更できます。この変更は、引数フィールドを SEM_NOALIGNMENTFAULTEXCEPT に設定した Win API 呼び出し SetErrorMode で行います。これにより、OS は整列エラーを処理できますが、"かなりの" パフォーマンス コストが発生します。1) この設定はプロセス単位であるため、各プロセスで最初の整列エラーの前に設定する必要があること、2) SEM_NOALIGNMENTFAULTEXCEPT は固定であるため、このビットがアプリケーションで SetErrorMode を通じて設定された場合は、アプリケーションの継続中に (不注意などで) リセットしてはならないことの 2 点に注意してください。
x86 アーキテクチャでは、オペレーティング システムは、整列エラーをアプリケーションから参照可能にしません。これらの 2 つのプラットフォームでは、整列エラーによりパフォーマンスも低下しますが、整列されていないデータを取得するためにハードウェアが複数のメモリ アクセスを行うので、Itanium の場合よりも重大度は大幅に低くなります。
x86-64 アーキテクチャでは、整列の例外は既定では無効になっており、修正はハードウェアで行われます。アプリケーションは、いくつかのレジスタ ビットを設定することで整列の例外を有効にでき、その場合は、ユーザーがオペレーティング システムで例外を SEM_NOALIGNMENTFAULTEXCEPT でマスクしていない限り例外が発生します (詳細については、『AMD x86-64 Architecture Programmer's Manual Volume 2: System Programming』を参照してください)。
x86 および x86-64 プラットフォームでは、不整列アクセスにより一般保護例外が生成されることがあります (これらは一般保護例外であり、整列チェック例外ではありません)。これは、不整列が 128 ビット型、特に SSE/SSE2 ベースの型で発生したことを示します。
実験した結果、リスト 4 のコードで (9,000,000 回の反復を使用し、0 および 3 のオフセットで整列と不整列をそれぞれ表しています)、Pentium III (731MHz、Microsoft Windows XP Professional を実行) の速度が低下し、不整列アクセスを行うプログラムは、整列アクセスを行うプログラムよりも実行速度が 3.25 倍低下することがわかりました。より高速な Pentium IV (2.53GHz、Windows XP Professional を実行) では、不整列アクセスを行うプログラムは、整列アクセスを行うプログラムよりも速度が約 2 倍低下しました。
これが望ましいパフォーマンスでないことは明白です。残念なことに、Itanium プロセッサ ファミリではさらに悪化します。Microsoft Windows Server 2003 を実行している 900MHz の Itanium2 で同じテスト (ただし、テストの実行時間が長いので 90,000 回だけ反復) を実行すると、不整列のプログラムの実行速度は 459 倍も低下します。このことからわかるように、内部ループで不整列アクセスを行うと、アプリケーションのパフォーマンスに多大な影響があります。
アプリケーションのクラッシュを防ぐ OS による修正を使用する場合でも、不整列アクセスを回避する必要があります。
リスト 4. 不整列と整列の OS による修正を比較するサンプル コード
#include <stdio.h>
#include <stdlib.h>
#include <sys/timeb.h>
#include <time.h>
#include <windows.h>
#ifdef _WIN64
#define UINT unsigned __int64
#define ENDPART QuadPart
#else
#define UINT unsigned int
#define ENDPART LowPart
#endif
int main(int argc, char* argv[])
{
SetErrorMode(GetErrorMode() | SEM_NOALIGNMENTFAULTEXCEPT);
UINT iters, offset;
if(argc < 2)
iters = 9000000;
else
iters = atoi(argv[1]);
if(argc < 3)
offset = 0;
else
offset = atoi(argv[2]);
printf("反復 = %d、オフセット = %d\n", iters, offset);
double *dest, *origsource;
double *source;
dest = new double[128];
origsource = new double[150];
source = (double *)((UINT)origsource + offset);
printf("宛先 = %x ソース = %x\n", dest, source);
LARGE_INTEGER startCount, endCount, freq;
QueryPerformanceFrequency(&freq);
QueryPerformanceCounter(&startCount);
for (UINT x = 0; x < iters; x++)
for(UINT i = 0; i < 128; ++i)
dest[i] = source[i];
QueryPerformanceCounter(&endCount);
printf("経過時間 = %lf\n操作の最適化を防ぐため %lf\n",
(double)(endCount.ENDPART-startCount.ENDPART)/freq.ENDPART, dest[75]);
delete[] origsource;
delete[] dest;
return 0;
}
コンパイラでの整列のサポート
明示的な構文を通じて、コンパイラがこれらの整列の問題を支援できる場合があります。ここでは、不整列アクセスのコストを最小化するため、または整列アクセスを保証するためにソース コードで使用できる拡張機能をいくつか示します。
__unaligned キーワード
前に述べたように、既定では、コンパイラはデータを自然境界に整列します。たいていの場合はこれで十分であり、問題は発生しませんが、明確な対処方法が存在しない (または、対処にかかる労力が大きすぎる) 整列の問題が発生する状況もあります。
プログラマが、どの変数が不整列境界でアクセスされる可能性があるのかを静的に判断できる場合は、__unaligned キーワード (同等のマクロは UNALIGNED) を使用して、これらの変数を不整列として指定できます。このキーワードを使用すると、コンパイラが不整列境界にある変数にアクセスするためのコードを挿入し、エラーが発生しないので便利です。コンパイラは、不整列境界に関して技巧を用いる余分なコードを挿入することによってこの処理を行いますが、コストがかからないわけではありません。余分な命令によりコードの実行速度が低下し、コード サイズが増えます。あいにく、これらの余分な命令は、データが整列されていると証明できる箇所にも生成されます。このため、このキーワードの使用には注意してください。
変数宣言で __unaligned キーワードを使用することにより、リスト 4 のプログラムを変更できます。この例では、source の宣言を次のように変更します。
__unaligned double *source;
このプログラムは、オペレーティング システムで整列エラーの修正を有効にしていない場合でも Itaniums で正しく実行されますが、パフォーマンスはある程度低下します。ただし、このパフォーマンス低下は、プログラム クラッシュや OS による修正で起こる重大なパフォーマンス低下ほど深刻ではありません (前に述べたように、コンパイラは、データが整列されていると証明できる箇所にも、不整列アクセスを処理するコードを挿入します。OS は、例外が発生した場合にだけ修正コードを実行し、例外は不整列アクセスが実際に発生した場合にだけ発生します)。
図 2 に、さまざまなデータ アクセス方法を使用した場合のリスト 4 のサンプル プログラムについて、Itanium 2 での実行時間を示します。プログラムは、データが整列され、__unaligned キーワードが使用されていない場合に最高の速度で実行されます。プログラムは、データが整列され、__unaligned キーワードが使用されている場合に、次に速い速度で実行されます (__unaligned キーワードを使用した場合は、データが整列されている場合でもパフォーマンスが低下します)。不整列データで __unaligned キーワードを使用した場合は、さらに少し速度が低下します。最後に、不整列データにアクセスし、SetErrorMode を SEM_NOALIGNMENTFAULTEXCEPT に設定している場合は、速度が大幅に低下します。

図 2. さまざまな種類のアクセスの結果を示すためのテスト プログラムの実行時間の比較 (y 軸の目盛りは log10 であることに注意してください)
__declspec(align(#))
既知の変数が不整列アクセスを行う場合の問題を扱いましたが、変数を自然境界とは異なる境界に割り当てる必要がある場合はどうすればよいでしょうか。たとえば、SSE2 命令を使用しているときに、オペランドを 16 バイト境界に整列する必要がある場合、または特定の変数をキャッシュ ライン境界に整列する必要がある場合があります。__declspec(align(#)) は、このような目的のために用意されています (# は 2 の累乗です)。リスト 5 に、その使用方法の例を示します。
リスト 5. __declspec(align(#)) の動作を示すコード
#include <stdio.h>
class ClassA {
public:
char d1;
__declspec(align(256)) char d2;
double d3;
};
int main()
{
__declspec(align(32)) double a;
double b;
__declspec(align(512)) char c;
ClassA d;
printf("sizeof(a) = %d、address(a) = %0x\n", sizeof(a), &a);
printf("sizeof(b) = %d、address(b) = %0x\n", sizeof(b), &b);
printf("sizeof(c) = %d、address(c) = %0x\n", sizeof(c), &c);
printf("sizeof(d) = %d、address(d.d2) = %0x\n", sizeof(d), &d.d2);
return 0;
}
出力は次のようになります (筆者のコンピュータでの結果です)。
sizeof(a) = 8, address(a) = 12fde0
sizeof(b) = 8, address(b) = 12fdd8
sizeof(c) = 1, address(c) = 12fa00
sizeof(d) = 512, address(d.d2) = 12f900
クラスの sizeof に注目してください。任意の構造体/クラスの sizeof の値は、最後のメンバのオフセットにそのメンバのサイズを加算し、最大メンバの整列値または構造体/クラス全体の整列値のどちらか大きい方の倍数に切り上げた値です (この定義は、MSDN の align のエントリから抽出したものです)。
CRT と組み込み
__declspec(align) は役に立つツールですが、動的データをヒープ外に整列することはできません。このため、C ランタイム ライブラリ (CRT) には、整列メモリ割り当てルーチンのセットが用意されています。これらのルーチンを次に示します (これらは <malloc.h> に含まれています)。
- void *_aligned_malloc(size_t size, size_t alignment)
- void *_aligned_offset_malloc(size_t size, size_t alignment, size_t offset)
- void _aligned_free(void *aligned_block)
- void *_aligned_realloc(void *aligned_block, size_t size, size_t alignment)
- void *_aligned_offset_realloc(void *aligned_block, size_t size, size_t alignment, size_t offset)
これらのルーチンの詳細については、MSDN ライブラリの「データの整列」を参照してください。
パフォーマンスを向上させる最善の方法の 1 つは、プログラマがチューニングに時間をかけたコードを使用することです。提供されている CRT メモリ ルーチン (strncpy、memcpy、memset、memmove など) はそのよい例です。CRT ルーチンは、特定のアーキテクチャに合わせてチューニングされた、手作業で作成されたルーチン (多くの場合はアセンブリ) であり、大規模な移動で不整列アクセスのコストが最小化されるようにソースと宛先を整列します。
また、ユーザーは、組み込みの生成を有効にする /Oi フラグまたは #pragma intrinsic(functions) プラグマも使用できます (/O2 フラグでは /Oi フラグが暗示されていることに注意してください)。組み込みは、コンパイラによって出力されるインライン ルーチンであり、一般にはアセンブリ言語の CRT ルーチンほどチューニングされていません。これらは、コード量の増加という追加コストを払って関数呼び出しのオーバーヘッドを回避します。/Oi または #pragma intrinsic の使用はコンパイラに対する推奨であり、コンパイラは組み込みまたは CRT ルーチンを自由に出力できることにも注意してください。何が生成されたかを判断するよい方法は、アセンブリ コードを見ることです。
IPF コンパイラは、インライン組み込みの展開を補助するために型情報も使用します。コンパイラは、ソースおよび宛先アドレスへのポインタの型を検証し、そこからこれらのアドレスの整列を推測します。ポインタの型が適切でない場合は、整列の例外が発生したり、(OS による修正で) プログラムの実行速度が低下したりすることがあります。
リスト 6 に、memcpy のコンパイラ組み込み、または CRT アセンブリ言語の手作業でチューニングしたルーチンを使用するコードでの、整列アクセスと不整列アクセスの結果を示します。CRT アセンブリ言語の手作業でチューニングしたルーチンを使用するには、#pragma function(function) プラグマを挿入してください。
リスト 6. 整列アクセスと不整列アクセスに対する組み込みルーチンと CRT ルーチンの結果を示すコード
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#include <windows.h>
#ifdef _WIN64
#define UINT unsigned __int64
#define ENDPART QuadPart
#else
#define UINT unsigned int
#define ENDPART LowPart
#endif
#pragma function(memcpy) // 組み込み生成では、この行をコメントにします。
int main(int argc, char *argv[])
{
int iters1 = atoi(argv[1]);
int size1 = atoi(argv[2]);
int offset = atoi(argv[3]);
char *source, *origsource = (char *)_aligned_malloc(size1, 8);
char *dest, *origdest = (char *)_aligned_malloc(size1, 8);
source = (char *)((UINT)origsource + offset);
dest = (char *)((UINT)origdest + offset);
LARGE_INTEGER startCount, endCount, freq;
QueryPerformanceFrequency(&freq);
QueryPerformanceCounter(&startCount);
for(int i = 0; i < iters1; ++i)
memcpy(dest, source, size1-offset);
QueryPerformanceCounter(&endCount);
printf("&source = %0x \t &dest = %0x\n", source, dest);
printf("経過時間 = %lf\n操作の最適化を防ぐため %lf\n",
(double)(endCount.ENDPART-startCount.ENDPART)/freq.ENDPART, dest[1]);
_aligned_free(source);
_aligned_free(dest);
}

図 3. Pentium III で整列データと不整列データに対して CRT ルーチンおよび組み込みルーチンを使用して memcpy を実行した場合の時間
図 3 と図 4 に、Pentium III コンピュータと Itanium2 コンピュータでさまざまなサイズの memcpy に対して 4 つの構成を実行した場合の相対的なパフォーマンスを示します。このデータは、次のパラメータを使用してリスト 6 のコードで生成しました。
exename 1000000 size offset
8 ≤ size ≤ 4096 および 0 ≤ offset ≤ 1 です。
Pentium III では、整列コピーについて、CRT ルーチンと組み込みルーチンのどちらを使用するかは大きな問題ではありません。ただし、大規模な不整列コピーでは、CRT ルーチンを使用する方がはるかに有利です。Itanium2 では、プログラマが /Oi または #pragma intrinsic を指定した場合でも、コンパイラはほぼ必ず CRT ルーチンを使用するので、CRT ルーチンだけを比較します。図 4 では、不整列と整列の CRT 呼び出しを比較します。整列データを使用すると、パフォーマンスが向上することが明らかです。ここでの結果は明白です。

図 4. Itanium2 で整列データと不整列データに対して CRT ルーチンを使用して memcoy を実行した場合の時間
整列の問題の回避方法に関するクイック ヒント
時間がないときにすばやく参照するには、このクイック ヒント セクションが役立ちます。ここでは、データの整列関連の問題の処理に役立つクイック ヒントを示します。
- 整列されたポインタ P1 からポインタ P2 にキャストし、TYPE_ALIGNMENT(P1) < TYPE_ALIGNMENT(P2) である場合は、すべてのアクセスが適切に整列されていることを確認する必要があります。P2 を使用して、P1 が指していたアドレスを逆参照すると、整列エラーが発生することがあります。ただし、TYPE_ALIGNMENT(P1) > TYPE_ALIGNMENT(P2) の場合は、P2 は、ポイントするすべての要素を要素単位で正しく逆参照できます。
- 領域の節約が有利であることが確実である場合 (たとえば、構造体を単純に転送するだけで、個々のメンバにはアクセスしない場合) 以外は、構造体をパックしないでください。
- データを整列する境界を理解します。整列を十分に高くしないと、整列の問題が発生することがありますが、整列を高く設定しすぎると、データが不必要に増える可能性があります。
命令の整列について
この記事も終わりに近づいたところで、読者の中には、「データの整列についてはわかったが、命令の整列についてはどうか。命令もメモリに格納されるのではないか」と思われている方がいるかもしれません。命令の整列も問題ではありますが、ほとんどのプログラマはこの問題を扱う必要がないので、この記事では説明していません。命令の整列は、主にコンパイラ作成者の問題です。命令の整列に関心を持つ "可能性のある " 一般的なプログラマとはアセンブリ言語プログラマであり、その中でも特にアセンブラを使用していないプログラマです。
まとめ
これまでの説明で、読者が Windows 開発を行うときにデータの整列の全詳細を確実に理解しているようになっていることを願っています。この記事では、多くのデータ整列エラーを回避する方法、データ整列エラーを回避できない場合に何をすればよいか、またそれらの対処方法に関連するさまざまなコストについて説明しました。この知識は、あらゆる Windows 開発に役立ちますが、x86 から Itanium にコードを移植する場合に特に役立ちます。この場合は、データの整列が中心的な役割を果たします。最終的には、より高速になり、コードの信頼性が高まります。
著者紹介
Kang Su Gatlin は、Microsoft の Visual C++ グループのプログラム マネージャです。UC San Diego の PhD を取得しており、 専門分野は高パフォーマンスの計算と最適化です。