我在 64 位机器上优化 memset 的尝试比标准实现需要更多时间。有人可以解释为啥吗?

Posted

技术标签:

【中文标题】我在 64 位机器上优化 memset 的尝试比标准实现需要更多时间。有人可以解释为啥吗?【英文标题】:My attempt to optimize memset on a 64bit machine takes more time than standard implementation. Can someone please explain why?我在 64 位机器上优化 memset 的尝试比标准实现需要更多时间。有人可以解释为什么吗? 【发布时间】:2014-01-02 22:09:34 【问题描述】:

(机器是运行 SL6 的 x86 64 位)

我试图查看是否可以在我的 64 位机器上优化 memset。根据我的理解,memset 逐字节地设置值。我假设如果我以 64 位为单位,它会更快。但不知何故,这需要更多时间。有人可以看看我的代码并提出原因吗?

/* Code */
#include <stdio.h>
#include <time.h>
#include <stdint.h>
#include <string.h>

void memset8(unsigned char *dest, unsigned char val, uint32_t count)

    while (count--)
        *dest++ = val;

void memset32(uint32_t *dest, uint32_t val, uint32_t count)

    while (count--)
        *dest++ = val;

void
memset64(uint64_t *dest, uint64_t val, uint32_t count)

    while (count--)
        *dest++ = val;

#define CYCLES 1000000000
int main()

    clock_t start, end;
    double total;
    uint64_t loop;
    uint64_t val;

    /* memset 32 */
    start = clock();
    for (loop = 0; loop < CYCLES; loop++) 
        val = 0xDEADBEEFDEADBEEF;
        memset32((uint32_t*)&val, 0, 2);
    
    end = clock();
    total = (double)(end-start)/CLOCKS_PER_SEC;
    printf("Timetaken memset32 %g\n", total);

    /* memset 64 */
    start = clock();
    for (loop = 0; loop < CYCLES; loop++) 
        val = 0xDEADBEEFDEADBEEF;
        memset64(&val, 0, 1);
    
    end = clock();
    total = (double)(end-start)/CLOCKS_PER_SEC;
    printf("Timetaken memset64 %g\n", total);

    /* memset 8 */
    start = clock();
    for (loop = 0; loop < CYCLES; loop++) 
        val = 0xDEADBEEFDEADBEEF;
        memset8((unsigned char*)&val, 0, 8);
    
    end = clock();
    total = (double)(end-start)/CLOCKS_PER_SEC;
    printf("Timetaken memset8 %g\n", total);

    /* memset */
    start = clock();
    for (loop = 0; loop < CYCLES; loop++) 
        val = 0xDEADBEEFDEADBEEF;
        memset(&val, 0, 8);
    
    end = clock();
    total = (double)(end-start)/CLOCKS_PER_SEC;
    printf("Timetaken memset %g\n", total);

    printf("-----------------------------------------\n");


/*Result*/
Timetaken memset32 12.46
Timetaken memset64 7.57
Timetaken memset8 37.12
Timetaken memset 6.03
-----------------------------------------

看起来标准 memset 比我的实现更优化。 我尝试查看代码,到处都可以看到 memset 的实现与我为 memset8 所做的相同。当我使用 memset8 时,结果更符合我的预期,与 memset 有很大不同。 有人可以建议我做错了什么吗?

【问题讨论】:

memset 将在可用时使用 sse2、sse4 和 avx 指令。别傻了,使用标准实现:) 你总是可以让你的编译器吐出汇编并比较你的实现和标准库之间的差异。此外,执行 memset8 测试用例似乎需要 37 秒的时间;您是否在打开优化的情况下进行编译? 看this implementation...这几乎是memset64的实现... @V-X:这是一个古老的参考实现。在 OSX 和 ios 上实际使用的实现完全不同(并且是用汇编编写的)。 除了其他人所说的之外,您还花费了大量时间计算 for 循环,而不是加载/存储。您应该填充一个大数组并查看结果 【参考方案1】:

实际的memset 实现通常在汇编中进行手动优化,并使用目标硬件上可用的最广泛对齐的写入。在 x86_64 上,至少有 16B 个存储(例如movaps)。它还可以利用预取(这在最近不太常见,因为大多数架构都有用于常规访问模式的良好自动流式预取器)、流式存储或专用指令(过去 rep stos 在 x86 上非常慢,但在 x86 上却相当快最近的微架构)。您的实现不做这些事情。系统实现速度更快应该不足为奇。

以 OS X 10.8(已在 10.9 中取代)中使用的implementation 为例。这是中等大小缓冲区的核心循环:

.align 4,0x90
1:  movdqa %xmm0,   (%rdi,%rcx)
    movdqa %xmm0, 16(%rdi,%rcx)
    movdqa %xmm0, 32(%rdi,%rcx)
    movdqa %xmm0, 48(%rdi,%rcx)
    addq   $64,      %rcx
    jne    1b

当在预 Haswell 微架构上以 16B/周期命中缓存时,此循环将使 LSU 饱和。基于 64 位存储(如 memset64)的实现不能超过 8B/周期(甚至可能无法实现,这取决于所讨论的微架构以及编译器是否展开您的循环)。在 Haswell 上,使用 AVX 存储或 rep stos 的实现可以更快,达到 32B/​​周期。

【讨论】:

【参考方案2】:

据我了解,memset 逐字节设置值。

memset 功能的详细信息取决于实现。依靠这一点通常是一件好事,因为我确信实现者对系统有广泛的了解,并且知道各种技术以使事情尽可能快。

为了详细说明,让我们看一下:

memset(&val, 0, 8);

当编译器看到这一点时,它会注意到以下几点:

填充值为0 要填充的字节数为 8

然后根据val&amp;val 的位置(在寄存器中、在内存中……)选择要使用的正确指令。但是如果memset 被卡住需要成为一个函数调用(就像你的实现一样),那么这些优化都是不可能的。即使它无法做出编译时决策,例如:

memset(&val, x, y); // no way to tell at compile time what x and y will be...

您可以放心,有一个用汇编程序编写的函数调用会在您的平台上尽可能快。

【讨论】:

我正在使用带有 gcc 编译器的 linux 和 glibc。这应该是开源和标准的。不是吗? 你能推荐一种更快的方法吗? memset 表现就好像它是一个字节一个字节。但今天所有称职的编译器都将其视为内置的,并且经常使用 SSE 或 AVX 指令。 @adikshit, memset 不一定是函数调用,而且似乎可以工作(至少对于 x86/x86_64 指令集),就像 Mysticial 所描述的一样(至少我最后一次看在它)。 我不确定假设每个内置的编译器都经过优化是好的。如果您看到 Agner Fog 的手册,他建议关闭 GCC 的内置内部函数 -fno-builtin。 memcpy 和 memset 函数在他测试的时候没有优化。他在汇编asmlib 中手动优化了几个函数,当时他的版本 memset 显然要快得多(尽管 Mac 版本表现良好)。也许从那时起 GCC 有所改进......【参考方案3】:

我认为值得探索如何编写更快的 memset,尤其是在 C/C++ 中使用 GCC(我假设您正在使用 Scientific Linux 6)。许多人认为标准实现是优化的。 这不一定是真的。如果您看到 Agner Fog 的Optimizing Software in C++ 手册的表 2.1,他会将几个不同编译器和平台的 memcpy 与他自己的汇编优化版本的 memcpy 进行比较。当时 GCC 中的 Memcpy 确实表现不佳(但 Mac 版本很好)。他声称内置功能更糟糕,并建议使用-no-builtin。以我的经验,GCC 非常擅长优化代码,但它的库函数(和内置函数)不是很优化(使用 ICC 则相反)。

看看使用内在函数能做多好会很有趣。如果您查看他的 asmlib,您会看到他如何使用 SSE 和 AVX 实现 memset(将其与 Apple 发布的优化版本 Stephen Canon 进行比较会很有趣)。

使用 AVX,您可以看到他一次写入 32 个字节。

K100: ; Loop through 32-bytes blocks. Register use is swapped
      ; Rcount = end of 32-bytes blocks part
      ; Rdest = negative index from the end, counting up to zero
      vmovaps [Rcount+Rdest], ymm0
      add     Rdest, 20H
      jnz     K100

在这种情况下,vmovaps 与内在 _mm256_store_ps 相同。也许从那时起 GCC 有所改进,但您可能能够使用内在函数击败 GCC 的 memset 实现。如果你没有 AVX,你肯定有 SSE(所有 x86 64 位都有),所以你可以查看他的代码的 SSE 版本,看看你能做什么。

这里是你的 memset32 函数的开始,假设数组适合 L1 缓存。如果数组不适合缓存,您希望使用_mm256_stream_ps 进行非临时存储。对于一般功能,您需要几种情况,包括内存不是 32 字节对齐的情况。

#include <immintrin.h>
int main() 

    int count = (1<<14)/sizeof(int);
    int* dest = (int*)_mm_malloc(sizeof(int)*count, 32); // 32 byte aligned

    int val = 0xDEADBEEFDEADBEEF;
    __m256 val8 = _mm256_castsi256_ps(_mm256_set1_epi32(val));
    for(int i=0; i<count; i+=8) 
        _mm256_store_ps((float*)(dest+i), val8);
    

【讨论】:

以上是关于我在 64 位机器上优化 memset 的尝试比标准实现需要更多时间。有人可以解释为啥吗?的主要内容,如果未能解决你的问题,请参考以下文章

如何编译程序以在 64 位机器上工作

用于长(64位)类型的C ++ memset()[重复]

在 64 位机器上,我可以安全地并行处理 64 位四字的各个字节吗?

如何在 64 位机器上以 32 位模式运行 VBScript?

x64 Windows Server 2003 上的远程调试

__int64 在 32 位机器上?