【问题标题】:Allocating initialized, aligned memory分配初始化的、对齐的内存
【发布时间】:2012-12-04 05:06:30
【问题描述】:

我正在编写一个程序(在 C++ 中),我需要在其中分配起始地址应与缓存行大小对齐的数组。当我分配这些数组时,我还希望内存初始化为零。

现在我使用 posix_memalign 函数让它工作。这适用于获取内存对齐的数组,但数组未初始化。有没有更好的函数可以在我初始化数组时将它们归零,还是我只需要编写一个单独的循环来为我做这件事?

【问题讨论】:

    标签: c++ c posix memory-alignment dynamic-allocation


    【解决方案1】:

    使用 GCC,mem_demo_1 编译为 60 行汇编,而 mem_demo_2 编译为 20 行,性能差异也很大。

    我决定在 Linux 2.6.32 上使用 gcc 4.4.6 验证此声明。 首先

    mem_demo_1 编译为 60 行汇编,而 mem_demo_2 编译 到 20

    .

    这是测试(在文件 main.c 中):

      #include <stdlib.h>
      #include <stdio.h>
      #include <string.h>
    
      char* mem_demo_1(char *j)
      {
          // *BAD* compiler cannot tell pointer alignment, must test
          memset(j, 0, 64);
          return j;
      }
    
      char* mem_demo_2(void)
      {
        // *GOOD* compiler can tell pointer alignment
        char * j = malloc(64);
        memset(j, 0, 64);
        return j;
      }
    
      int main()
      {
        char *p;
        p = malloc(64);
        p = mem_demo_1(p);
        printf ("%p\n",p);
        free (p);
    
        p = mem_demo_2();
        printf ("%p\n",p);
        free (p);
    
        return 0;
      }
    

    当我编译时:

      gcc -fno-inline -fno-builtin -m64 -g -O2 main.c -o main.no_inline_no_builtin  
    

    我看到mem_demo_1里面只有8行:

    (gdb) disassemble mem_demo_1
    Dump of assembler code for function mem_demo_1:
       0x00000000004005d0 <+0>:     push   %rbx
       0x00000000004005d1 <+1>:     mov    $0x40,%edx
       0x00000000004005d6 <+6>:     mov    %rdi,%rbx
       0x00000000004005d9 <+9>:     xor    %esi,%esi
       0x00000000004005db <+11>:    callq  0x400470 <memset@plt>
       0x00000000004005e0 <+16>:    mov    %rbx,%rax
       0x00000000004005e3 <+19>:    pop    %rbx
       0x00000000004005e4 <+20>:    retq
    End of assembler dump.
    

    我看到mem_demo_2里面只有11行:

    (gdb) disassemble mem_demo_2
    Dump of assembler code for function mem_demo_2:
       0x00000000004005a0 <+0>:     push   %rbx
       0x00000000004005a1 <+1>:     mov    $0x40,%edi
       0x00000000004005a6 <+6>:     callq  0x400480 <malloc@plt>
       0x00000000004005ab <+11>:    mov    $0x40,%edx
       0x00000000004005b0 <+16>:    mov    %rax,%rbx
       0x00000000004005b3 <+19>:    xor    %esi,%esi
       0x00000000004005b5 <+21>:    mov    %rax,%rdi
       0x00000000004005b8 <+24>:    callq  0x400470 <memset@plt>
       0x00000000004005bd <+29>:    mov    %rbx,%rax
       0x00000000004005c0 <+32>:    pop    %rbx
       0x00000000004005c1 <+33>:    retq
    End of assembler dump.
    

    因此,“mem_demo_1 编译为 60 行汇编,而 mem_demo_2 编译为 20”无法确认。

    当我编译时:

      gcc -m64 -g -O2 main.c -o main.default
    

    gcc 使用自己的 memset 实现,并且 mem_demo_1 和 mem_demo_2 两个函数都更大:

    mem_demo_1: 43 instructions
    mem_demo_2: 48 instructions
    

    但是,“mem_demo_1 编译为 60 行汇编,而 mem_demo_2 编译为 20”也无法确认。

    第二个

    “性能差异也很大”

    我扩展了 main.c 以便使用 memset 进行大量循环。我也没有看到 mem_demo_1 中的 memset 比 mem_demo_2 中的慢。 这是来自 Linux 性能报告:
    mem_demo_2 在 memset 中花费了 8.37%:

    8.37% main.perf.no_bu libc-2.12.so [.] __memset_sse2

    而 mem_demo_1 在 memset 中花费了 7.61%:

    7.61% main.perf.no_bu libc-2.12.so [.] __memset_sse2

    这些本身就是测量值:

    # time ./main.perf.no_builtin_no_inline 100000000 1 0
    number loops 100000000
    mem_demo_1
    
    real    0m3.483s
    user    0m3.481s
    sys     0m0.002s
    
    # time ./main.perf.no_builtin_no_inline 100000000 2 0
    number loops 100000000
    mem_demo_2
    
    real    0m3.503s
    user    0m3.501s
    sys     0m0.001s
    

    顺便说一句,gcc -fverbose-asm -c -S -O3 向我展示了 mem_demo_2 的汇编器:

    char* mem_demo_2(void)
    {
      char * j = malloc(64);
      memset(j, 0, 64);
      return j;
    }
    
            .file   "main.mem_demo_2.c"
    # GNU C (GCC) version 4.4.6 20110731 (Red Hat 4.4.6-3) (x86_64-redhat-linux)
    #       compiled by GNU C version 4.4.6 20110731 (Red Hat 4.4.6-3), GMP version 4.3.1, MPFR version 2.4.1.
    # GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
    # options passed:  main.mem_demo_2.c -m64 -mtune=generic -auxbase-strip
    # main.mem_demo_2.default.asm -g -O3 -fverbose-asm
    # options enabled:  -falign-loops -fargument-alias
    # -fasynchronous-unwind-tables -fauto-inc-dec -fbranch-count-reg
    # -fcaller-saves -fcommon -fcprop-registers -fcrossjumping
    # -fcse-follow-jumps -fdefer-pop -fdelete-null-pointer-checks
    # -fdwarf2-cfi-asm -fearly-inlining -feliminate-unused-debug-types
    # -fexpensive-optimizations -fforward-propagate -ffunction-cse -fgcse
    # -fgcse-after-reload -fgcse-lm -fguess-branch-probability -fident
    # -fif-conversion -fif-conversion2 -findirect-inlining -finline
    # -finline-functions -finline-functions-called-once
    # -finline-small-functions -fipa-cp -fipa-cp-clone -fipa-pure-const
    # -fipa-reference -fira-share-save-slots -fira-share-spill-slots -fivopts
    # -fkeep-static-consts -fleading-underscore -fmath-errno -fmerge-constants
    # -fmerge-debug-strings -fmove-loop-invariants -fomit-frame-pointer
    # -foptimize-register-move -foptimize-sibling-calls -fpeephole -fpeephole2
    # -fpredictive-commoning -freg-struct-return -fregmove -freorder-blocks
    # -freorder-functions -frerun-cse-after-loop -fsched-interblock
    # -fsched-spec -fsched-stalled-insns-dep -fschedule-insns2 -fsigned-zeros
    # -fsplit-ivs-in-unroller -fsplit-wide-types -fstrict-aliasing
    # -fstrict-overflow -fthread-jumps -ftoplevel-reorder -ftrapping-math
    # -ftree-builtin-call-dce -ftree-ccp -ftree-ch -ftree-coalesce-vars
    # -ftree-copy-prop -ftree-copyrename -ftree-cselim -ftree-dce
    # -ftree-dominator-opts -ftree-dse -ftree-fre -ftree-loop-im
    # -ftree-loop-ivcanon -ftree-loop-optimize -ftree-parallelize-loops=
    # -ftree-pre -ftree-reassoc -ftree-scev-cprop -ftree-sink -ftree-sra
    # -ftree-switch-conversion -ftree-ter -ftree-vect-loop-version
    # -ftree-vectorize -ftree-vrp -funit-at-a-time -funswitch-loops
    # -funwind-tables -fvar-tracking -fvar-tracking-assignments
    # -fvect-cost-model -fverbose-asm -fzero-initialized-in-bss
    # -m128bit-long-double -m64 -m80387 -maccumulate-outgoing-args
    # -malign-stringops -mfancy-math-387 -mfp-ret-in-387 -mfused-madd -mglibc
    # -mieee-fp -mmmx -mno-sse4 -mpush-args -mred-zone -msse -msse2
    # -mtls-direct-seg-refs
    mem_demo_2:
    .LFB30:
            .file 1 "main.mem_demo_2.c"
            .loc 1 6 0
            .cfi_startproc
            subq    $8, %rsp
            .cfi_def_cfa_offset 16
            .loc 1 7 0
            movl    $64, %edi
            call    malloc
            .loc 1 8 0
            testb   $1, %al
            .loc 1 7 0
            movq    %rax, %rsi
    .LVL0:
            .loc 1 8 0
            movq    %rax, %rdi
            movl    $64, %edx
            jne     .L10
            testb   $2, %dil
            jne     .L11
    .L3:
            testb   $4, %dil
            jne     .L12
    .L4:
            movl    %edx, %ecx
            xorl    %eax, %eax
    .LVL1:
            shrl    $3, %ecx
            testb   $4, %dl
            mov     %ecx, %ecx
            rep stosq
            je      .L5
            movl    $0, (%rdi)
            addq    $4, %rdi
    .L5:
            testb   $2, %dl
            je      .L6
            movw    $0, (%rdi)
            addq    $2, %rdi
    .L6:
            andl    $1, %edx
            je      .L7
            movb    $0, (%rdi)
    .L7:
            .loc 1 10 0
            movq    %rsi, %rax
            addq    $8, %rsp
            .cfi_remember_state
            .cfi_def_cfa_offset 8
            ret
            .p2align 4,,10
            .p2align 3
    .L10:
            .cfi_restore_state
            .loc 1 8 0
            leaq    1(%rax), %rdi
            movb    $0, (%rax)
            movb    $63, %dl
            testb   $2, %dil
            je      .L3
            .p2align 4,,10
            .p2align 3
    .L11:
            movw    $0, (%rdi)
            addq    $2, %rdi
            subl    $2, %edx
            testb   $4, %dil
            je      .L4
            .p2align 4,,10
            .p2align 3
    .L12:
            movl    $0, (%rdi)
            subl    $4, %edx
            addq    $4, %rdi
            jmp     .L4
            .cfi_endproc
    

    【讨论】:

    • 你为什么告诉它不要内联?重点是衡量memset 的性能,您明确告诉它不要优化memset。是的,这样一来,他们都会表现得很糟糕。它们都包括跳转到通用的memset,它不对指针对齐做任何假设。关键是要尝试在至少一种情况下获得好的代码,而你在两种情况下都做得不好。
    • @David Schwart 我也启用了内联。请在我的帖子中看到这个gcc -m64 -g -O2 main.c -o main.default
    • 我不确定您为什么会看到不同的结果。我粘贴了一些有关如何获得结果的更多详细信息online
    • @David Schwartz 更新了我的答案 - 为 mem_demo_2 添加了汇编程序。它比你的更大。
    • 我在 Windows XP 上使用 MinGW gcc 4.6.2 编译了相同的程序。当我使用gcc -O3 -g main.c -o main 编译时,我看不出函数之间有任何区别。当我用gcc -march=native -O3 -g main.c -o main.native 编译时,我得到了你所说的行数的差异。所以,-march=i386-march=core2 的区别很大
    【解决方案2】:

    只需在区块上致电memset。确保在调用memset 之前不要将指针转换为设置成本高的类型(如char *)。由于您的指针将对齐,因此请确保该信息不会对编译器隐藏。

    更新:为了澄清我关于不隐藏对齐的观点,比较:

    char* mem_demo_1(char *j)
    { // *BAD* compiler cannot tell pointer alignment, must test
        memset(j, 0, 64);
        return j;
    }
    
    char* mem_demo_2(void)
    { // *GOOD* compiler can tell pointer alignment
        char * j = malloc(64);
        memset(j, 0, 64);
        return j;
    }
    

    GCCmem_demo_1 编译为 60 行汇编,而mem_demo_2 编译为 20。性能差异也很大。

    【讨论】:

    • 你能解释一下Make sure you don't cast the pointer to a type that's expensive to set (like char *) before calling memset吗?
    • @skwllsp 我认为他的意思是char 太小了。
    • 谢谢!使用 memset 清除字符数组有什么问题?是什么让某些类型比其他类型更贵?
    • @martega:如果您将char * 传递给memset,编译器无法对对齐做出任何假设。如果将long * 传递给memset,编译器可以假定内存块在long 边界上对齐,这使得memset 更加更有效率。
    • @David Schwartz。请看看我的回答。如果您对此发表评论,我将不胜感激。
    猜你喜欢
    • 2012-01-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-12-27
    • 2018-02-18
    • 1970-01-01
    • 2015-11-08
    相关资源
    最近更新 更多