ルンディンに追加。はい、レジスタベースのインデックスがあるARMのような命令セットや、即時インデックスを使用する一部のリーチは、コンパイラがインデックスを使用するように奨励するのに役立つ場合があります。また、たとえばARMはロード命令でポインタレジスタをインクリメントできますが、基本的に* p ++を1つの命令でインクリメントできます。
p[i] または p[i++] と *p または *p++ を使用すると、常にトスアップになります。一部の命令セットは、どちらのパスを使用するかがはるかに明白です。
同様にあなたのインデックス。アップの代わりにカウントダウンを使用していない場合は、ループごとに命令を節約できます。これを行う人もいます:
inc reg
cmp reg,#7
bne loop_top
ループごとに命令を保存するかもしれませんが、カウントダウンしている場合:
dec reg
bne loop_top
または私が知っている1つのプロセッサでさえ
decrement_and_jump_if_not_zero loop_top
通常、コンパイラはこれを認識しており、推奨する必要はありません。ただし、メモリの読み取り順序が重要な p[i] 形式を使用する場合、コンパイラは読み取りの順序を任意に変更できないか、少なくとも変更すべきではありません。したがって、その場合、コードをカウントダウンする必要があります。
だから私はこれらすべてのことを試しました:
unsigned fun1 ( const unsigned char *p, unsigned *x )
{
unsigned sum;
unsigned sqsum;
int i;
unsigned f;
sum = 0;
sqsum = 0;
for(i=0; i<8; i++)
{
f = *p++;
sum += f;
sqsum += f*f;
}
//to keep the compiler from optimizing
//stuff out
x[0]=sum;
return(sqsum);
}
unsigned fun2 ( const unsigned char *p, unsigned *x )
{
unsigned sum;
unsigned sqsum;
int i;
unsigned f;
sum = 0;
sqsum = 0;
for(i=8;i--;)
{
f = *p++;
sum += f;
sqsum += f*f;
}
//to keep the compiler from optimizing
//stuff out
x[0]=sum;
return(sqsum);
}
unsigned fun3 ( const unsigned char *p, unsigned *x )
{
unsigned sum;
unsigned sqsum;
int i;
sum = 0;
sqsum = 0;
for(i=0; i<8; i++)
{
sum += (unsigned)p[i];
sqsum += ((unsigned)p[i])*((unsigned)p[i]);
}
//to keep the compiler from optimizing
//stuff out
x[0]=sum;
return(sqsum);
}
unsigned fun4 ( const unsigned char *p, unsigned *x )
{
unsigned sum;
unsigned sqsum;
int i;
sum = 0;
sqsum = 0;
for(i=8; i;i--)
{
sum += (unsigned)p[i-1];
sqsum += ((unsigned)p[i-1])*((unsigned)p[i-1]);
}
//to keep the compiler from optimizing
//stuff out
x[0]=sum;
return(sqsum);
}
gcc と llvm (clang) の両方で。もちろん、ループは定数だったので、どちらもループを展開しました。gcc では、微妙なレジスタ ミックスの変更の場合、各実験で同じコードが生成されます。そして、少なくとも 1 つの読み取りがコードで記述された順序ではなかったので、バグであると主張します。
4つすべてのgccソリューションはこれで、一部の読み取りの並べ替えがあり、ソースコードからの読み取りが順不同であることに注意してください。これが、コードで記述された順序での読み取りに依存するハードウェア/ロジックに反する場合、大きな問題が発生します。
00000000 <fun1>:
0: e92d05f0 push {r4, r5, r6, r7, r8, sl}
4: e5d06001 ldrb r6, [r0, #1]
8: e00a0696 mul sl, r6, r6
c: e4d07001 ldrb r7, [r0], #1
10: e02aa797 mla sl, r7, r7, sl
14: e5d05001 ldrb r5, [r0, #1]
18: e02aa595 mla sl, r5, r5, sl
1c: e5d04002 ldrb r4, [r0, #2]
20: e02aa494 mla sl, r4, r4, sl
24: e5d0c003 ldrb ip, [r0, #3]
28: e02aac9c mla sl, ip, ip, sl
2c: e5d02004 ldrb r2, [r0, #4]
30: e02aa292 mla sl, r2, r2, sl
34: e5d03005 ldrb r3, [r0, #5]
38: e02aa393 mla sl, r3, r3, sl
3c: e0876006 add r6, r7, r6
40: e0865005 add r5, r6, r5
44: e0854004 add r4, r5, r4
48: e5d00006 ldrb r0, [r0, #6]
4c: e084c00c add ip, r4, ip
50: e08c2002 add r2, ip, r2
54: e082c003 add ip, r2, r3
58: e023a090 mla r3, r0, r0, sl
5c: e080200c add r2, r0, ip
60: e5812000 str r2, [r1]
64: e1a00003 mov r0, r3
68: e8bd05f0 pop {r4, r5, r6, r7, r8, sl}
6c: e12fff1e bx lr
ロードのインデックスと微妙なレジスタの混合は、gcc との関数間の唯一の違いであり、すべての操作は同じ順序で同じでした。
llvm/clang:
00000000 <fun1>:
0: e92d41f0 push {r4, r5, r6, r7, r8, lr}
4: e5d0e000 ldrb lr, [r0]
8: e5d0c001 ldrb ip, [r0, #1]
c: e5d03002 ldrb r3, [r0, #2]
10: e5d08003 ldrb r8, [r0, #3]
14: e5d04004 ldrb r4, [r0, #4]
18: e5d05005 ldrb r5, [r0, #5]
1c: e5d06006 ldrb r6, [r0, #6]
20: e5d07007 ldrb r7, [r0, #7]
24: e08c200e add r2, ip, lr
28: e0832002 add r2, r3, r2
2c: e0882002 add r2, r8, r2
30: e0842002 add r2, r4, r2
34: e0852002 add r2, r5, r2
38: e0862002 add r2, r6, r2
3c: e0870002 add r0, r7, r2
40: e5810000 str r0, [r1]
44: e0010e9e mul r1, lr, lr
48: e0201c9c mla r0, ip, ip, r1
4c: e0210393 mla r1, r3, r3, r0
50: e0201898 mla r0, r8, r8, r1
54: e0210494 mla r1, r4, r4, r0
58: e0201595 mla r0, r5, r5, r1
5c: e0210696 mla r1, r6, r6, r0
60: e0201797 mla r0, r7, r7, r1
64: e8bd41f0 pop {r4, r5, r6, r7, r8, lr}
68: e1a0f00e mov pc, lr
おそらく、キャッシュについて考えて、読み取りをすべて一度に行うことができます。llvm は、少なくとも 1 つのケースで読み取りの順序が狂っていました。
00000144 <fun4>:
144: e92d40f0 push {r4, r5, r6, r7, lr}
148: e5d0c007 ldrb ip, [r0, #7]
14c: e5d03006 ldrb r3, [r0, #6]
150: e5d02005 ldrb r2, [r0, #5]
154: e5d05004 ldrb r5, [r0, #4]
158: e5d0e000 ldrb lr, [r0]
15c: e5d04001 ldrb r4, [r0, #1]
160: e5d06002 ldrb r6, [r0, #2]
164: e5d00003 ldrb r0, [r0, #3]
はい、ラムからいくつかの値を平均化するために、順序は問題ではありません。
そのため、コンパイラは展開されたパスを選択し、マイクロ最適化を気にしませんでした。ループのサイズのため、両方ともループごとにロードされた値の 1 つを保持する一連のレジスタを焼き付けてから、それらの一時的な読み取りからの加算または乗算を実行することを選択します。ループのサイズを少し大きくすると、展開されたループがレジスターを使い果たしたときに sum と sqsum の累積が見られると予想されます。または、ループを展開しないことを選択した場所でしきい値に達します。
長さを渡し、上記のコードの 8 を渡された長さに置き換えると、コンパイラはこれからループを作成するように強制されます。最適化が表示されます。次のような命令が使用されます。
a4: e4d35001 ldrb r5, [r3], #1
そして、アームであるため、ループレジスタを1か所で変更し、後で命令数と等しくない場合は分岐します...可能なためです。
確かにこれは数学関数ですが、float を使用するのは面倒です。そして、乗算を使用するのは苦痛であり、除算はさらに悪化します。幸いなことに、シフトが使用されました。幸いなことに、これはシフトを使用できるように符号なしでした(符号付きの数値に対して除算を使用した場合、利用可能な場合、コンパイラは算術シフトを使用することを知っていたはずです)。
したがって、内部ループは複数回実行されるため、基本的には内部ループのマイクロ最適化に焦点を当てます。これを変更できる場合は、可能であればシフトして追加するか、データを配置してループから取り出すことができます (可能であれば)。 、これを行うために他のコピーループを他の場所で無駄にしないでください)
const unsigned char* p = (const unsigned char*)(j*patch->step + aux );
速度を上げることができます。私はそれを試しませんでしたが、ループ内のループであるため、コンパイラはおそらくそのループを展開しません...
簡単に言えば、命令セットによっては、愚かなコンパイラに対していくらかの利益が得られる可能性がありますが、このコードはそれほど悪くないため、コンパイラは可能な限り最適化できます。