将参数作为编译时常量或变量传递时函数性能之间的差异
difference between the function performance when passing parameter as compile time constant or variable
在Linux内核代码中有一个用于测试位的宏(Linux版本2.6.2):
#define test_bit(nr, addr) \
(__builtin_constant_p((nr)) \
? constant_test_bit((nr), (addr)) \
: variable_test_bit((nr), (addr)))
其中 constant_test_bit
和 variable_test_bit
定义为:
static inline int constant_test_bit(int nr, const volatile unsigned long *addr )
{
return ((1UL << (nr & 31)) & (addr[nr >> 5])) != 0;
}
static __inline__ int variable_test_bit(int nr, const volatile unsigned long *addr)
{
int oldbit;
__asm__ __volatile__(
"btl %2,%1\n\tsbbl %0,%0"
:"=r" (oldbit)
:"m" (ADDR),"Ir" (nr));
return oldbit;
}
我明白__builtin_constant_p
是用来检测一个变量是编译时常量还是未知的。我的问题是:当参数是否为编译时常量时,这两个函数之间是否存在性能差异?为什么有的时候用C版,没有的时候用汇编版?
更新: 下面的主要函数用于测试性能:
常量,调用constant_test_bit:
int main(void) {
unsigned long i, j = 21;
unsigned long cnt = 0;
srand(111)
//j = rand() % 31;
for (i = 1; i < (1 << 30); i++) {
j = (j + 1) % 28;
if (constant_test_bit(j, &i))
cnt++;
}
if (__builtin_constant_p(j))
printf("j is a compile time constant\n");
return 0;
}
这正确输出句子 j is a...
对于其他情况,只需取消注释将 "random" 数字分配给 j
的行,并相应地更改函数名称。当取消注释该行时,输出将为空,这是预期的。
我用gcc test.c -O1
编译,结果如下:
不变,constant_test_bit:
$ time ./a.out
j is compile time constant
real 0m0.454s
user 0m0.450s
sys 0m0.000s
常量,variable_test_bit(省略time ./a.out
,下同):
j is compile time constant
real 0m0.885s
user 0m0.883s
sys 0m0.000s
变量,constant_test_bit:
real 0m0.485s
user 0m0.477s
sys 0m0.007s
变量,variable_test_bit:
real 0m3.471s
user 0m3.467s
sys 0m0.000s
每个版本我都跑过几次,上面的结果是它们的典型值。似乎 constant_test_bit
函数总是比 variable_test_bit
函数快,无论参数是否是编译时间常量......对于最后两个结果(当 j
不是constant )可变版本甚至比常量版本慢得多。
我在这里遗漏了什么吗?
使用godbolt we can do a experiment using of constant_test_bit,使用-O3
标志编译了以下两个测试函数gcc
:
// Non constant expression test case
int func1(unsigned long i, unsigned long j)
{
int x = constant_test_bit(j, &i) ;
return x ;
}
// constant expression test case
int func2(unsigned long i)
{
int x = constant_test_bit(21, &i) ;
return x ;
}
我们看到优化器能够将常量表达式优化为以下情况:
shrq , %rax
andl , %eax
而非常量表达式的情况如下:
sarl , %eax
andl , %ecx
cltq
leaq -8(%rsp,%rax,8), %rax
movq (%rax), %rax
shrq %cl, %rax
andl , %eax
所以优化器能够为常量表达式情况生成更好的代码,我们可以看到 constant_test_bit
的非常量情况与 [=18= 中的手动汇编相比非常糟糕] 并且实施者必须相信 constant_test_bit
的常量表达式情况最终优于:
btl %edi,8(%rsp)
sbbl %esi,%esi
对于大多数情况。
至于为什么您的测试用例似乎显示出不同的结论,是因为您的测试用例存在缺陷。我没能解决所有问题。但是,如果我们使用带有非常量表达式的 constant_test_bit
查看 this case,我们可以看到优化器能够将所有工作移到外观之外,并减少与 constant_test_bit
相关的工作在外观内部循环到:
movq (%rax), %rdi
即使使用较旧的 gcc
版本,但这种情况可能与使用 test_bit
的情况无关。可能会有更具体的情况,这种优化不会有可能。
在Linux内核代码中有一个用于测试位的宏(Linux版本2.6.2):
#define test_bit(nr, addr) \
(__builtin_constant_p((nr)) \
? constant_test_bit((nr), (addr)) \
: variable_test_bit((nr), (addr)))
其中 constant_test_bit
和 variable_test_bit
定义为:
static inline int constant_test_bit(int nr, const volatile unsigned long *addr )
{
return ((1UL << (nr & 31)) & (addr[nr >> 5])) != 0;
}
static __inline__ int variable_test_bit(int nr, const volatile unsigned long *addr)
{
int oldbit;
__asm__ __volatile__(
"btl %2,%1\n\tsbbl %0,%0"
:"=r" (oldbit)
:"m" (ADDR),"Ir" (nr));
return oldbit;
}
我明白__builtin_constant_p
是用来检测一个变量是编译时常量还是未知的。我的问题是:当参数是否为编译时常量时,这两个函数之间是否存在性能差异?为什么有的时候用C版,没有的时候用汇编版?
更新: 下面的主要函数用于测试性能:
常量,调用constant_test_bit:
int main(void) {
unsigned long i, j = 21;
unsigned long cnt = 0;
srand(111)
//j = rand() % 31;
for (i = 1; i < (1 << 30); i++) {
j = (j + 1) % 28;
if (constant_test_bit(j, &i))
cnt++;
}
if (__builtin_constant_p(j))
printf("j is a compile time constant\n");
return 0;
}
这正确输出句子 j is a...
对于其他情况,只需取消注释将 "random" 数字分配给 j
的行,并相应地更改函数名称。当取消注释该行时,输出将为空,这是预期的。
我用gcc test.c -O1
编译,结果如下:
不变,constant_test_bit:
$ time ./a.out
j is compile time constant
real 0m0.454s
user 0m0.450s
sys 0m0.000s
常量,variable_test_bit(省略time ./a.out
,下同):
j is compile time constant
real 0m0.885s
user 0m0.883s
sys 0m0.000s
变量,constant_test_bit:
real 0m0.485s
user 0m0.477s
sys 0m0.007s
变量,variable_test_bit:
real 0m3.471s
user 0m3.467s
sys 0m0.000s
每个版本我都跑过几次,上面的结果是它们的典型值。似乎 constant_test_bit
函数总是比 variable_test_bit
函数快,无论参数是否是编译时间常量......对于最后两个结果(当 j
不是constant )可变版本甚至比常量版本慢得多。
我在这里遗漏了什么吗?
使用godbolt we can do a experiment using of constant_test_bit,使用-O3
标志编译了以下两个测试函数gcc
:
// Non constant expression test case
int func1(unsigned long i, unsigned long j)
{
int x = constant_test_bit(j, &i) ;
return x ;
}
// constant expression test case
int func2(unsigned long i)
{
int x = constant_test_bit(21, &i) ;
return x ;
}
我们看到优化器能够将常量表达式优化为以下情况:
shrq , %rax
andl , %eax
而非常量表达式的情况如下:
sarl , %eax
andl , %ecx
cltq
leaq -8(%rsp,%rax,8), %rax
movq (%rax), %rax
shrq %cl, %rax
andl , %eax
所以优化器能够为常量表达式情况生成更好的代码,我们可以看到 constant_test_bit
的非常量情况与 [=18= 中的手动汇编相比非常糟糕] 并且实施者必须相信 constant_test_bit
的常量表达式情况最终优于:
btl %edi,8(%rsp)
sbbl %esi,%esi
对于大多数情况。
至于为什么您的测试用例似乎显示出不同的结论,是因为您的测试用例存在缺陷。我没能解决所有问题。但是,如果我们使用带有非常量表达式的 constant_test_bit
查看 this case,我们可以看到优化器能够将所有工作移到外观之外,并减少与 constant_test_bit
相关的工作在外观内部循环到:
movq (%rax), %rdi
即使使用较旧的 gcc
版本,但这种情况可能与使用 test_bit
的情况无关。可能会有更具体的情况,这种优化不会有可能。