分配已初始化、对齐的内存

9
我正在编写一个程序(使用C ++),我需要分配数组,其起始地址应与缓存行大小对齐。当我分配这些数组时,我还希望将内存初始化为零。
目前,我使用posix_memalign函数使其正常工作,可以获得在内存对齐的数组,但这些数组未初始化。是否有更好的函数可用于在初始化它们时将数组清零?或者我只能写一个单独的循环来实现这个功能?
2个回答

9

只需对该块调用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;
}

使用 GCC 编译,mem_demo_1 会编译成60行汇编代码,而 mem_demo_2 只有20行。性能差异也非常大。


1
请解释一下“在调用memset之前,确保不要将指针转换为设置成本昂贵的类型(如char *)”。 - user184968
谢谢!使用memset清除字符数组有什么问题?是什么使某些类型比其他类型更昂贵? - martega
1
如果你将一个 char * 传递给 memset 函数,编译器无法对其进行任何对齐假设。如果你将一个 long * 传递给 memset 函数,编译器可以假定内存块按照 long 字节对齐,并且这使得 memset 函数更加高效。 - David Schwartz
@David Schwartz。请看一下我的回答。如果您能评论一下,我将不胜感激。 - user184968
你竟然把两种情况都比我最坏的情况还要糟糕!在你的答案中,它们都需要跳转到通用的 memset,而该函数不对内存对齐作出任何假设。因此,你不仅得到了最坏情况下的 memset,还多了一个跳转和返回操作! - David Schwartz
显示剩余2条评论

0
使用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 perf报告的:
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

3
为什么你告诉它不要内联?本意是为了测试memset的性能,你明确告诉它不要优化memset。是的,这样两个都表现很差。它们都包含跳转到通用memset的操作,该操作不做指针对齐的任何假设。目的是尝试在至少一个情况下得到好的代码,但你却在两种情况下都得到了糟糕的结果。 - David Schwartz
@David Schwart 我也启用了内联。请看我在帖子中的这行代码 gcc -m64 -g -O2 main.c -o main.default - user184968
@David Schwartz 更新了我的回答 - 添加了 mem_demo_2 的汇编代码。它比您的要大。 - user184968
嗯,不知怎么的,你的编译器无法对 mem_demo_2 进行优化。也许你正在使用的 GCC 版本太老了,不知道如何处理。然而,mem_demo_1 甚至连这个机会都没有给予它。 - David Schwartz
1
我在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时有很大的区别。 - user184968
显示剩余2条评论

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