添加到伦丁。是的,像 ARM 这样的指令集,你有一个基于寄存器的索引或一些直接索引,你可能会受益于鼓励编译器使用索引。此外,例如 ARM 可以在加载指令中增加其指针寄存器,基本上 *p++ 在一条指令中。
使用 p[i] 或 p[i++] 与 *p 或 *p++ 总是一个折腾,一些指令集更明显地采用哪条路径。
同样你的索引。如果你不使用它倒计时而不是倒计时可以节省每个循环的指令,也许更多。有些人可能会这样做:
inc reg
cmp reg,#7
bne loop_top
如果您正在倒计时,但您可能会在每个循环中保存一条指令:
dec reg
bne loop_top
甚至我知道的一个处理器
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,为每个实验生成相同的代码,以防寄存器组合发生细微的变化。我会争辩一个错误,因为其中至少有一个错误不是按照代码描述的顺序进行的。
所有四个的 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 的读取也出现了乱序。
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]
是的,为了对 ram 中的一些值进行平均,顺序不是问题,继续前进。
所以编译器选择展开的路径并且不关心微优化。由于循环的大小,两者都选择烧毁一堆寄存器,每个循环都保存一个加载的值,然后从这些临时读取或乘法中执行加法。如果我们稍微增加循环的大小,我希望在展开的循环中看到 sum 和 sqsum 累积,因为它用完了寄存器,否则将达到他们选择不展开循环的阈值。
如果我传入长度,并将上面代码中的 8 替换为传入的长度,则强制编译器对此进行循环。您可以看到优化,使用如下指令:
a4: e4d35001 ldrb r5, [r3], #1
并且作为手臂,他们在一个地方对循环寄存器进行了修改,如果不等于稍后的指令数量则分支......因为他们可以。
当然这是一个数学函数,但使用浮点数很痛苦。并且使用乘法是痛苦的,分裂更糟,幸运的是使用了转变。幸运的是,这是无符号的,因此您可以使用移位(如果您对有符号数使用除法,编译器将/应该知道使用算术移位)。
所以基本上关注内部循环的微优化,因为它会运行多次,如果可以改变它,那么它会变成移位和添加,如果可能的话,或者安排数据以便你可以把它从循环中取出(如果可能的话,不要在其他地方浪费其他复制循环来执行此操作)
const unsigned char* p = (const unsigned char*)(j*patch->step + aux );
你可以得到一些速度。我没有尝试过,但因为它是循环中的循环,编译器可能不会展开该循环......
长话短说,您可能会根据针对笨拙编译器的指令集获得一些收益,但是这段代码并不是很糟糕,因此编译器可以尽可能地优化它。