将参数作为编译时常量或变量传递时,函数性能的差异

7
在 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_bitvariable_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...

对于其他情况,请取消注释将随机数赋给j的那一行,并相应更改函数名称。取消注释后,输出结果将为空,这是正常现象。

我使用gcc test.c -O1进行编译,以下是结果:

constant、constant_test_bit:

$ time ./a.out 

j is compile time constant

real    0m0.454s
user    0m0.450s
sys     0m0.000s

常量,变量_test_bit(省略time ./a.out,以下同):

j is compile time constant

real    0m0.885s
user    0m0.883s
sys     0m0.000s

变量,常量_test_bit:

real    0m0.485s
user    0m0.477s
sys     0m0.007s

变量,变量测试位:

real    0m3.471s
user    0m3.467s
sys     0m0.000s

我运行了每个版本多次,上面的结果是它们的典型值。似乎无论参数是否为编译时常量,constant_test_bit函数始终比variable_test_bit函数更快...对于最后两个结果(当j不是常量时),变量版本甚至比常量版本慢得多。 我错过了什么吗?


可能是,但唯一的方法是进行测量。 - deviantfan
显然,有人认为这会影响性能,否则就不会有两个版本。关于细节,你需要考虑4种情况(将常量/非常量传递给任一函数)。你认为每种情况会发生什么?你看过生成的汇编代码吗? - Marc Glisse
@deviantfan,我已经添加了性能结果。 - Xiangyu Zhu
@MarcGlisse,这就是我正在考虑的。也许稍后会花些时间研究汇编代码。 - Xiangyu Zhu
注意,你没有使用这些函数的“从大数组中提取一个位”的能力,这是汇编版本可能会胜出(或不胜)的地方。 - Ben Voigt
1个回答

5

使用godbolt,我们可以进行一个常量测试位的实验,以下两个测试函数使用gcc编译,并加上-O3标志:

// 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    $21, %rax
andl    $1, %eax

当非常量表达式的情况如下:
sarl    $5, %eax
andl    $31, %ecx
cltq
leaq    -8(%rsp,%rax,8), %rax
movq    (%rax), %rax
shrq    %cl, %rax
andl    $1, %eax

因此,优化器能够为常量表达式情况生成更好的代码,我们可以看到与variable_test_bit中手动编写的汇编相比,constant_test_bit的非常量情况相当糟糕,实现者必须相信constant_test_bit的常量表达式情况最终会更好。
btl %edi,8(%rsp)
sbbl %esi,%esi 

对于大多数情况而言。

至于为什么你的测试案例似乎得出了不同的结论,那是因为你的测试案例有缺陷。我还没有能够找出所有问题。但是如果我们看一下this case,使用非常量表达式的constant_test_bit,我们可以看到优化器能够将所有工作移动到循环外,并将与constant_test_bit相关的工作在循环内减少到:

movq    (%rax), %rdi

即使使用旧版的gcc,但这种情况可能与使用test_bit的情况无关。可能存在更特定的情况,在这种情况下,这种优化不可能实现。

但是如果参数是非常量的,为什么要使用变量版本呢?在这种情况下,gcc生成的常量版本汇编似乎仍然比变量版本更好。 - Xiangyu Zhu
@XiangyuZhu 更新了我的答案,你的测试存在一些问题,很难将它们全部解决,但很可能最初选择这种方法的原因是我在最初回答中提出的原因。我希望他们使用相关案例进行了基准测试,并确保假设确实适用,但很难确定。 - Shafik Yaghmour
是的,也许我的代码测试方式非常“特殊”,以至于gcc能够比variable_test_bit更好地进行优化。我查看了最近的内核代码,并发现类似的代码仍然存在,因此必须有一个很好的理由(使用不同版本的函数)。但是,我决定暂时不去管它。感谢您的回答。 - Xiangyu Zhu
2
@XiangyuZhu 自从Linux开始优化以来,优化已经大大改善,可能手动优化不再像过去那样重要,但要弄清楚这一点需要了解具体的用例。这种做法的危险在于人们可能会盲目地复制这些优化而没有对其进行基准测试。 - Shafik Yaghmour

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接