为什么 glibc 的 memcpy 没有选择 AVX512 版本?

4

我按照以下方式编译示例代码:

 #cat array_addition.c 
 #define MAX 1000000
 #define S 1024
 #include <string.h>
int a[S], b[S], c[S];

__attribute__((target_clones("avx512f", "avx2","arch=atom","default")))
void foo(int argc){
    int i,x;

for (x=0; x<1024; x++){
    for (i=0; i<S; i++){
        a[i] = b[i] + c[i];
    }
}
    b[0] = argc;
    memcpy(&a[0], &b[0], argc *sizeof(int));
}
int main(int argc, char** argv) {
    foo(argc);
    return 0;
}

这里调用了 memcpy 函数。

从 objdump 中可以发现,它将调用 GLIBC 的 memcpy 函数:

#readelf -r a.out 

Relocation section '.rela.dyn' at offset 0x418 contains 1 entry:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000403ff8  000200000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0

Relocation section '.rela.plt' at offset 0x430 contains 4 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000404018  000100000007 R_X86_64_JUMP_SLO 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0
000000404020  000200000007 R_X86_64_JUMP_SLO 0000000000000000 __gmon_start__ + 0
000000404028  000300000007 R_X86_64_JUMP_SLO 0000000000000000 memcpy@GLIBC_2.14 + 0
000000404030  000000000025 R_X86_64_IRELATIV                    4018f0

然后,我使用gdb来跟踪它使用了哪个glibc实现;

(gdb) b memcpy@plt    
Breakpoint 1 at 0x401050
(gdb) s
The program is not being run.
(gdb) r
Starting program: /root/a.out 

Breakpoint 1, 0x0000000000401050 in memcpy@plt ()
(gdb) s
Single stepping until exit from function memcpy@plt,
which has no line number information.
0x00007ffff7b623a0 in __memcpy_ssse3_back () from /lib64/libc.so.6
(gdb) info function __memcpy_*
All functions matching regular expression "__memcpy_*":

Non-debugging symbols:
0x00007ffff7aa2840  __memcpy_chk_sse2
0x00007ffff7aa2850  __memcpy_sse2
0x00007ffff7ab1b40  __memcpy_chk_avx512_no_vzeroupper
0x00007ffff7ab1b50  __memcpy_avx512_no_vzeroupper
0x00007ffff7b23360  __memcpy_chk
0x00007ffff7b5a470  __memcpy_chk_ssse3
0x00007ffff7b5a480  __memcpy_ssse3
0x00007ffff7b62390  __memcpy_chk_ssse3_back
0x00007ffff7b623a0  __memcpy_ssse3_back
(gdb) 

有 __memcpy_avx512_no_zeroupper 这个函数,但没有被选择;

而我的 CPU 支持它的功能:

Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb cat_l3 cdp_l3 invpcid_single pti intel_ppin ssbd mba ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid ept_ad fsgsbase tsc_adjust bmi1 hle avx2 smep bmi2 erms invpcid rtm cqm mpx rdt_a avx512f avx512dq rdseed adx smap clflushopt clwb intel_pt avx512cd avx512bw avx512vl xsaveopt xsavec xgetbv1 xsaves cqm_llc cqm_occup_llc cqm_mbm_total cqm_mbm_local dtherm ida arat pln pts pku ospke flush_l1d

gcc 版本:

使用内建规范。COLLECT_GCC=gcc COLLECT_LTO_WRAPPER=/root/china-gcc-10.2.0/libexec/gcc/x86_64-pc-linux-gnu/10.2.0/lto-wrapper 目标:x86_64-pc-linux-gnu 已配置为:./configure --prefix=/root/china-gcc-10.2.0 --disable-multilib 线程模型:posix 支持的 LTO 压缩算法:zlib gcc 版本 10.2.0 (GCC)


Glibc是自由软件。您可以下载其源代码,研究它并改进它。 - Basile Starynkevitch
1个回答

7
在像Skylake-X和IceLake这样的“主流”CPU上,仅当您在程序的大部分运行时间内始终使用512位向量时,才值得使用它们,而不仅仅是偶尔进行memcpy。(如果您的程序不会长时间运行,否则您将通过上下文切换和/或超线程减慢共享同一物理核心的其他进程。)有关详细信息,请参见 SIMD指令降低CPU频率:您不希望偶尔调用memcpy将CPU频率保持在较低的最大睿频水平。
对于某些事情,例如掩码很好或者使用YMM16..31来避免VZEROUPPER,使用AVX-512特性与256位向量(AVX-512VL)可能值得一试。
我猜想glibc只会在像Knight's Landing (KNL) Xeon Phi这样的系统上将memcpy解析为__memcpy_avx512_no_vzeroupper,因为该CPU是围绕AVX-512设计的,并且使用512位ZMM向量没有任何不利影响。在KNL上即使使用ymm0..15后也没有必要使用vzeroupper。实际上,在KNL上vzeroupper非常慢,绝对需要避免,因此将no_vzeroupper放入函数名称中。 https://code.woboq.org/userspace/glibc/sysdeps/x86_64/multiarch/memmove-avx512-no-vzeroupper.S.html是该版本的源代码。它使用ZMM向量,包括ZMM0..15,因此如果在Skylake/IceLake CPU上使用,则应该使用vzeroupper。 该版本似乎是为KNL设计的。

如果有一个AVX-512VL版本,使用ymm16..31来避免vzeroupper(以加快32..64字节的复制),而从不使用ZMM寄存器,则会带来某些微小的好处。

对于__memcpy_avx512_no_vzeroupper来说,只使用ZMM16..31是有意义的,这样避免vzeroupper在主流CPU上不会成为问题;然后它将成为已经大量使用AVX-512(因此已经支付了CPU频率成本)的代码中可用的选项。


那么 __memcpy_sse3 对我的 CPU 最好?如果要复制的内存非常大,glibc 是否会使用更高性能的方法,如 vmovntq?希望澄清选择不同实现的策略 :) - Chinaxing
@Chinaxing:我本来期望在你的Skylake-X系统上看到__memcpy_avx_unaligned_erms,就像我的Skylake台式机一样。(https://code.woboq.org/userspace/glibc/sysdeps/x86_64/multiarch/memmove-avx-unaligned-erms.S.html)。也许你的发行版配置有误,省略了AVX1/`rep movsb版本。 ssse3`版本甚至避免了256位向量。 - Peter Cordes
这取决于glibc版本吗?我使用的是glibc 2.17。可能太旧了吗? - Chinaxing
@Chinaxing:不太可能;毕竟你的glibc已经足够新,支持AVX512!AVX1早在2011年就存在了好几年。 - Peter Cordes
1
@PeterCordes 最近由于 vzeroupper 中止事务,他们为几乎所有字符串函数添加了 zmm16...31 版本。Memmove commit - Noah
非常有趣的问题和答案。+1 - user997112

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