为什么__builtin_popcount比我自己的比特计数功能慢?

Why is __builtin_popcount slower than my own bit counting function?

本文关键字:功能 自己的 builtin popcount 我自己 为什么      更新时间:2023-10-16

在我编写了自己的位计数例程后,我偶然发现了 gcc 的__builtin_popcount。 但是当我切换到__builtin_popcount我的软件实际上运行得更慢了。 我在英特尔酷睿 i3-4130T CPU @ 2.90GHz 上的 Unbutu 上。 我构建了一个性能测试,看看有什么。它看起来像这样:

#include <iostream>
#include <sys/time.h>
#include <stdint.h>
using namespace std;
const int bitCount[256] = {
0,1,1,2,1,2,2,3,  1,2,2,3,2,3,3,4,  1,2,2,3,2,3,3,4,  2,3,3,4,3,4,4,5,
1,2,2,3,2,3,3,4,  2,3,3,4,3,4,4,5,  2,3,3,4,3,4,4,5,  3,4,4,5,4,5,5,6,
1,2,2,3,2,3,3,4,  2,3,3,4,3,4,4,5,  2,3,3,4,3,4,4,5,  3,4,4,5,4,5,5,6,
2,3,3,4,3,4,4,5,  3,4,4,5,4,5,5,6,  3,4,4,5,4,5,5,6,  4,5,5,6,5,6,6,7,
1,2,2,3,2,3,3,4,  2,3,3,4,3,4,4,5,  2,3,3,4,3,4,4,5,  3,4,4,5,4,5,5,6,
2,3,3,4,3,4,4,5,  3,4,4,5,4,5,5,6,  3,4,4,5,4,5,5,6,  4,5,5,6,5,6,6,7,
2,3,3,4,3,4,4,5,  3,4,4,5,4,5,5,6,  3,4,4,5,4,5,5,6,  4,5,5,6,5,6,6,7,
3,4,4,5,4,5,5,6,  4,5,5,6,5,6,6,7,  4,5,5,6,5,6,6,7,  5,6,6,7,6,7,7,8
};
const uint32_t m32_0001 = 0x000000ffu;
const uint32_t m32_0010 = 0x0000ff00u;
const uint32_t m32_0100 = 0x00ff0000u;
const uint32_t m32_1000 = 0xff000000u;
inline int countBits(uint32_t bitField)
{
return
bitCount[(bitField & m32_0001)      ] +
bitCount[(bitField & m32_0010) >>  8] +
bitCount[(bitField & m32_0100) >> 16] +
bitCount[(bitField & m32_1000) >> 24];
}
inline long long currentTime() {
struct timeval ct;
gettimeofday(&ct, NULL);
return ct.tv_sec * 1000000LL + ct.tv_usec;
}
int main() {
long long start, delta, sum;
start = currentTime();
sum = 0;
for(unsigned i = 0; i < 100000000; ++i)
sum += countBits(i);
delta = currentTime() - start;
cout << "countBits         : sum=" << sum << ": time (usec)=" << delta << endl;
start = currentTime();
sum = 0;
for(unsigned i = 0; i < 100000000; ++i)
sum += __builtin_popcount(i);
delta = currentTime() - start;
cout << "__builtin_popcount: sum=" << sum << ": time (usec)=" << delta << endl;
start = currentTime();
sum = 0;
for(unsigned i = 0; i < 100000000; ++i) {
int count;
asm("popcnt %1,%0" : "=r"(count) : "rm"(i) : "cc");
sum += count;
}
delta = currentTime() - start;
cout << "assembler         : sum=" << sum << ": time (usec)=" << delta << endl;
return 0;
}

起初,我用一个较旧的编译器运行它:

> g++ --version | head -1
g++ (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4
> cat /proc/cpuinfo | grep 'model name' | head -1
model name      : Intel(R) Core(TM) i3-4130T CPU @ 2.90GHz
> g++ -O3 popcountTest.cpp
> ./a.out
countBits         : sum=1314447104: time (usec)=148506
__builtin_popcount: sum=1314447104: time (usec)=345122
assembler         : sum=1314447104: time (usec)=138036

如您所见,基于表的countBits几乎与汇编程序一样快,并且比__builtin_popcount快得多。 然后我在不同的机器类型(相同的处理器 - 我认为主板也相同(上尝试了更新的编译器:

> g++ --version | head -1
g++ (Ubuntu 7.3.0-16ubuntu3) 7.3.0
> cat /proc/cpuinfo | grep 'model name' | head -1
model name      : Intel(R) Core(TM) i3-4130T CPU @ 2.90GHz
> g++ -O3 popcountTest.cpp
> ./a.out
countBits         : sum=1314447104: time (usec)=164247
__builtin_popcount: sum=1314447104: time (usec)=345167
assembler         : sum=1314447104: time (usec)=138028

奇怪的是,较旧的编译器比较新的编译器更好地优化了我的countBits功能,但它仍然与汇编器相比具有优势。 显然,既然汇编程序行编译和运行,我的处理器支持popcount,但为什么__builtin_popcount慢了两倍多呢? 我自己的例程怎么可能与硅基流行音乐竞争? 我对其他用于查找第一个设置位等的例程有相同的经验。 我的例程都比GNU"内置"的等价物快得多。

(顺便说一句,我不知道如何编写汇编程序。我刚刚在某个网页上找到了那行,它奇迹般地似乎有效。

如果在命令行上没有指定适当的"-march",gcc 会生成对__popcountdi2函数的调用,而不是对popcnt指令的调用。请参阅:https://godbolt.org/z/z1BihM

根据维基百科,自Nehalem以来,POPCNT由英特尔支持,自巴塞罗那以来由AMD支持:https://en.wikipedia.org/wiki/SSE4#POPCNT_and_LZCNT

我认为在将 -march=native 添加到编译行(如 Mat 和 Alan Birtles 所建议的那样(后共享新的性能结果可能会很有用,这允许使用 popcount 机器指令。 结果因编译器版本而异。 这是较旧的编译器:

> g++ --version | head -1
g++ (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4
> cat /proc/cpuinfo | grep 'model name' | head -1
model name      : Intel(R) Core(TM) i3-4130T CPU @ 2.90GHz
> g++ -march=native -O3 popcountTest.cpp
> ./a.out
countBits         : sum=1314447104: time (usec)=163947
__builtin_popcount: sum=1314447104: time (usec)=138046
assembler         : sum=1314447104: time (usec)=138036

这是较新的编译器:

> g++ --version | head -1
g++ (Ubuntu 7.3.0-16ubuntu3) 7.3.0
> cat /proc/cpuinfo | grep 'model name' | head -1
model name      : Intel(R) Core(TM) i3-4130T CPU @ 2.90GHz
> g++ -march=native -O3 popcountTest.cpp
> ./a.out
countBits         : sum=1314447104: time (usec)=163133
__builtin_popcount: sum=1314447104: time (usec)=73987
assembler         : sum=1314447104: time (usec)=138036

观察:

  1. 将 -march=native 添加到旧版 g++ 的命令行中 编译器将__builtin_popcount的性能提高到相等 汇编程序的,并将我的计数位例程减少了大约 15%。

  2. 将 -march=native 添加到较新的 g++ 的命令行中 编译器导致__builtin_popcount的性能超越 汇编程序的。 我认为这与 我与汇编程序一起使用的堆栈变量,尽管我不确定。 那里 对我的计数位性能没有影响(如我的中所述 问题,这个较新的编译器已经变慢了。

我偶然发现了这个,我想它可能能够分享更现代和令人惊讶的结果。

在装有英特尔 i7 7920HQ 的 MacOS 12.2 上,当使用 -O3 -march=native 使用 clang++ 13 进行编译时,结果如下:

countBits         : sum=1314447104: time (usec)=93142
__builtin_popcount: sum=1314447104: time (usec)=59412
assembler         : sum=1314447104: time (usec)=111535

因此,对于现代 CPU 和现代编译器,使用 __builtin_popcount 总是有意义的。