繁体   English   中英

我在64位机器上优化memset的尝试比标准实现花费更多时间。 有人可以解释一下原因吗?

[英]My attempt to optimize memset on a 64bit machine takes more time than standard implementation. Can someone please explain why?

(机器是x86 64位运行SL6)

我试图看看我是否可以在我的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实现通常在汇编时手动优化,并使用目标硬件上可用的最宽对齐写入。 在x86_64上将至少有16B个商店( movapsmovaps )。 它也可能利用预取(最近这种情况不太常见,因为大多数架构都有常规访问模式的良好自动流预取器),流媒体存储或专用指令(历史上的rep stos在x86上速度非常慢,但最近速度相当快微体系结构)。 你的实现不会做这些事情。 系统实现速度更快不应该是非常令人惊讶的。

例如,考虑OS X 10.8中使用的实现 (已在10.9中取代)。 这是适度大小缓冲区的核心循环:

.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

当在16B /周期的Haswell前微架构上达到高速缓存时,该循环将使LSU饱和。 基于像memset64这样的64位存储的memset64不能超过8B /周期(甚至可能无法实现,具体取决于所讨论的微体系结构以及编译器是否展开循环)。 在Haswell上,使用AVX存储或rep stos实现可以更快地实现32B /周期。

根据我的理解,memset逐字节地设置并设置值。

memset工具的功能细节取决于实现。 依靠这通常是一件好事,因为我确信实现者对系统有广泛的了解并且知道所有类型的技术以尽可能快地完成任务。

再详细说明一下,让我们来看看:

memset(&val, 0, 8);

当编译器看到它时,它可以注意到一些事情,如:

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

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

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

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

我认为值得探索如何编写更快的memset,特别是在C / C ++中使用GCC(我假设您使用的是Scientific Linux 6)。 许多人认为标准实现已经过优化。 这不一定是真的。 如果您在C ++手册中看到Agner Fog的优化软件的表2.1,他将几个不同编译器和平台的memcpy与他自己的汇编优化版memcpy进行了比较。 当时海湾合作委员会的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

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

假设数组适合L1缓存,这是memset32函数的开始。 如果数组不适合缓存,则需要使用_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);
    }
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM