我需要以最有效(最快)的方式来弹出大小为128位的无符号变量。
尽管解决方案是更多可移植的,甚至更好。
首先,GCC中有两种类型,分别是__uint128_t
和unsigned __int128
。我猜他们最终还是一样,没有理由写丑的unsigned __int128
东西,所以尽管它应该是新类型,但我更喜欢第一个,它与标准{{ 1}}。另外,英特尔拥有uint64_t
,这是使用它的另一个原因(可移植性)。
我写了以下代码:
__uint128_t
这绝对是最快的选择吗?
编辑:
我想到了另一个选择,它可能(也可能不是)更快:
#include <nmmintrin.h>
#include <stdint.h>
static inline uint_fast8_t popcnt_u128 (__uint128_t n)
{
const uint64_t n_hi = n >> 64;
const uint64_t n_lo = n;
const uint_fast8_t cnt_hi = _mm_popcnt_u64(n_hi);
const uint_fast8_t cnt_lo = _mm_popcnt_u64(n_lo);
const uint_fast8_t cnt = cnt_hi + cnt_lo;
return cnt;
}
这样,尽管我不知道它是否合法(是吗?(编辑:它是Type punning between integer and array using `union`?)),但我会避免这种转变。
答案 0 :(得分:7)
使用GCC和clang,如果您删除了static inline
,则两个函数都将编译为相同的asm ,并且可能等效地内联。
我建议使用unsigned
,因为在x86-64 Linux上sizeof(uint_fast8_t)
= 1。 _fast
类型提出了“针对什么目的快速”的问题; fast8非常适合数组中的紧凑存储,fast32
是一种64位类型,可以避免为指针数学避免重做符号或零扩展名,但是浪费了数组中的空间。
clang知道两个popcnt结果的总和适合8位整数而没有溢出,因此即使将结果求和到unsigned
计数器中,它也可以优化零扩展名,但是gcc不会。 (例如,将返回类型更改为unsigned
,您将获得一条额外的movzx eax, dil
指令。)硬件popcnt
指令产生的结果正确地零扩展为64位,但是分配uint_fast8_t
(也称为uint8_t
)明确要求编译器将结果截断为8位。
x86-64 System V ABI允许args和返回值中包含大量垃圾,因此当返回类型较窄时,该函数的独立版本可以允许将EAX的高位进位。
我会避免这种转变。
仅在C源代码中存在该移位。在asm中,高/低半部分将存储在单独的64位寄存器或单独的内存源操作数中。
来自the Godbolt compiler explorer
# gcc8.3 -O3 -march=haswell for the union and the shift version
popcnt_u128:
xor eax, eax # break popcnt's false dependency on Intel CPUs
popcnt rsi, rsi # _mm_popcnt_u64(n_hi);
popcnt rax, rdi # popcnt(lo)
add eax, esi # clang uses add al,cl and doesn't avoid false deps except in a loop
ret # return value in AL (low 8 bits of EAX)
GCC可以通过同时执行两个popcnts并使用lea eax, [rdi + rsi]
来避免异或归零。但是您说了一些关于数组的内容,因此,如果数据来自内存,则GCC通常会先进行移动加载,然后再进行popcnt以避免错误的依赖关系。 (Why does breaking the "output dependency" of LZCNT matter?)或实际上,它会将目标异或为零,然后使用内存源popcnt,该代码大小可能会稍小。
我不信任__builtin_popcountll,因为它使用long long而不是uint64_t。我认为创建一个处理位并使用非固定宽度的类型的函数很疯狂。我不知道海湾合作委员会的人们在想什么。
它实际上使用unsigned long long
,而不是签名的long long
; 那会疯了。
unsigned long long
至少为 64位,并且uint64_t
必须为64位。 (实际上,仅在类型完全为64位且没有填充的C实现中存在;对它的支持是可选的)。我不确定GNU C是否支持unsigned long long
不 64位或uint64_t
不可用的任何目标。甚至是int64_t
,也必须是2的补码。 (如果GCC支持任何非2的补码目标,则为IDK。)
您可以将输入强制转换为uint64_t
,以确保没有设置更高的位。从uint64_t
到unsigned long long
的隐式转换不会设置任何额外的位,即使在ULL
宽于64位的平台上也是如此。
例如 __builtin_popcountll( (uint64_t)n );
将始终安全地计算n
的低64位,而不考虑unsigned long long
的宽度。
我正在使用一个非常大的静态数组。我是否需要关心缓存,还是GCC会为我处理?我认为这只是malloc和类似问题。 GCC在编译时就知道该数组,因此它可以比我做得更好。
GCC将(几乎?)永远不会重新排列循环以更改内存访问模式。静态数组与malloc
ed内存没有实质性区别;他们不会免费保持高速缓存。有关更多信息,请参见What Every Programmer Should Know About Memory?。
但是,如果您只是顺序地遍历内存并弹出整个数组,那么是否使用__uint128_t
并没有关系。
clang将使用具有AVX2 __builtin_popcntll
(作为半字节LUT)在阵列上自动_mm_popcnt_u64
或vpshufb
向量化,这在包括Broadwell在内的Intel CPU上非常有用。参见Counting 1 bits (population count) on large data using AVX-512 or AVX-2
但是不幸的是,将包装函数用于__uint128_t
数组会失败。请参阅Godbolt链接中的最后两个功能。