以下是在x86-64上设置C中的单个位的两种方法:
inline void SetBitC(long *array, int bit) {
//Pure C version
*array |= 1<<bit;
}
inline void SetBitASM(long *array, int bit) {
// Using inline x86 assembly
asm("bts %1,%0" : "+r" (*array) : "g" (bit));
}
将GCC 4.3与-O3 -march=core2
选项一起使用时,与常量bit
一起使用时,C版本需要 90%的时间。 (两个版本编译为完全相同的汇编代码,但C版本使用or [1<<num],%rax
指令而不是bts [num],%rax
指令)
当与变量bit
一起使用时,C版本表现更好,但仍然明显慢于内联汇编。
重置,切换和检查位具有类似的结果。
为什么GCC对这种常见操作的优化程度如此之差?我是否在使用C版本做错了什么?
编辑对不起,等待很长时间,这是我用来进行基准测试的代码。它实际上是一个简单的编程问题......
int main() {
// Get the sum of all integers from 1 to 2^28 with bit 11 always set
unsigned long i,j,c=0;
for (i=1; i<(1<<28); i++) {
j = i;
SetBit(&j, 10);
c += j;
}
printf("Result: %lu\n", c);
return 0;
}
gcc -O3 -march=core2 -pg test.c
./a.out
gprof
with ASM: 101.12 0.08 0.08 main
with C: 101.12 0.16 0.16 main
time ./a.out
也会给出类似的结果。
答案 0 :(得分:13)
为什么GCC对这种常见操作的优化程度如此之差?
序言:自20世纪80年代后期以来,对编译器优化的关注已经从专注于个人操作的微基准测试转移到专注于人们关注速度的应用程序的macrobenchmarks。目前,大多数编译器编写者都专注于宏基准测试,开发好的基准测试套件是值得认真考虑的事情。
回答:gcc上没有人使用基准,其中or
和bts
之间的差异对实际程序的执行时间很重要。如果你能够制作这样的节目,你可能会得到gcc-land中人们的关注。
我是否在使用C版本做错了什么?
不,这是非常好的标准C.实际上非常易读和惯用。
答案 1 :(得分:2)
您可以发布用于执行时间的代码吗?这种操作可能很难准确到达时间。
理论上两个代码序列应该同样快,所以最可能的解释(在我看来)是某些东西导致你的计时代码给出虚假结果。
答案 2 :(得分:2)
对于这样的代码:
#include <stdio.h>
#include <time.h>
int main() {
volatile long long i = 0;
time_t start = time (NULL);
for (long long n = 0; n < (1LL << 32); n++) {
i |= 1 << 10;
}
time_t end = time (NULL);
printf("C took %ds\n", (int)(end - start));
start = time (NULL);
for (long long n = 0; n < (1LL << 32); n++) {
__asm__ ("bts %[bit], %[i]"
: [i] "=r"(i)
: "[i]"(i), [bit] "i" (10));
}
end = time (NULL);
printf("ASM took %ds\n", (int)(end - start));
}
结果是:
C took 12s
ASM took 10s
我的旗帜是(-std=gnu99 -O2 -march=core2
)。没有挥发性,循环被优化了。 gcc 4.4.2。
没有区别:
__asm__ ("bts %[bit], %[i]"
: [i] "+m"(i)
: [bit] "r" (10));
所以可能答案是 - 没人关心。在microbenchmark中,唯一的区别是这两种方法之间的区别,但在现实生活中,我相信这样的代码不占用太多CPU。
此外代码:
#include <stdio.h>
#include <time.h>
int main() {
volatile long long i = 0;
time_t start = time (NULL);
for (long long n = 0; n < (1L << 32); n++) {
i |= 1 << (n % 32);
}
time_t end = time (NULL);
printf("C took %ds\n", (int)(end - start));
start = time (NULL);
for (long long n = 0; n < (1L << 32); n++) {
__asm__ ("bts %[bit], %[i]"
: [i] "+m"(i)
: [bit] "r" (n % 32));
}
end = time (NULL);
printf("ASM took %ds\n", (int)(end - start));
}
结果是:
C took 9s
ASM took 10s
两项结果都“稳定”。测试CPU'Intel(R)Core(TM)2 Duo CPU T9600 @ 2.80GHz'。
答案 3 :(得分:1)
这是一种非常常见的嵌入式系统操作,通常受资源限制。对于这样的系统,10 Cycles vs 5 Cycles是一个令人讨厌的性能损失。在许多情况下,人们想要访问IO端口或使用16或32位寄存器作为布尔位标志来节省内存。
事实是,if(bit_flags& 1<<12)
更具可读性[并且在使用库时可移植]而不是程序集等效。同样地,IO_PINS|= 1<<5;
不幸的是,这些速度慢很多,因此笨拙的asm宏仍然存在。
在许多方面,嵌入式和用户空间应用程序的目标是相反的。外部通信(对用户界面或机器接口)的响应性不太重要,同时确保控制回路(例如微小标记)在最短时间内完成是绝对关键的,可以建立或破坏选定的处理器或控制策略。
显然,如果一个人能负担得起多GHz的CPU以及所有相关的外围设备,芯片组等需要支持它,那么根本不需要担心低级优化。实时控制系统中的1000倍速微控制器意味着节省时钟周期的重要性要高出1000倍。
答案 4 :(得分:0)
我认为你问了很多优化器。
您可以通过执行`register long z = 1L&lt;&lt; bit;“,然后用你的数组或那个。
但是,我假设90%的时间,你的意思是C版本需要10个周期而asm版本需要5个周期,对吗?性能如何在-O2或-O1进行比较?