原子加载和存储函数产生的汇编代码与非原子加载和存储相同。

6
为什么store_idx_x86()的汇编输出与store_idx()相同,load_idx_x86()的汇编输出与load_idx()相同?
我理解__atomic_load_n()将刷新核心的失效队列,而__atomic_store_n()将刷新核心的存储缓冲区。
注意——我遵守了:gcc(GCC) 4.8.2 20140120(Red Hat 4.8.2-16)
更新:我明白x86永远不会重新排序存储与其他存储以及加载与其他加载——那么gcc是否足够聪明地只在需要时实现sfence和lfence,或者使用__atomic_会产生栅栏(假设内存模型比__ATOMIC_RELAXED更严格)?
代码
#include <stdint.h>


inline void store_idx_x86(uint64_t* dest, uint64_t idx)
{   
    *dest = idx;    
}

inline void store_idx(uint64_t* dest, uint64_t idx)
{
    __atomic_store_n(dest, idx, __ATOMIC_RELEASE);
}

inline uint64_t load_idx_x86(uint64_t* source)
{
    return *source;

}

inline uint64_t load_idx(uint64_t* source)
{
    return __atomic_load_n(source, __ATOMIC_ACQUIRE);
}

汇编:

.file   "util.c"
    .text
    .globl  store_idx_x86
    .type   store_idx_x86, @function
store_idx_x86:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movq    %rdi, -8(%rbp)
    movq    %rsi, -16(%rbp)
    movq    -8(%rbp), %rax
    movq    -16(%rbp), %rdx
    movq    %rdx, (%rax)
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   store_idx_x86, .-store_idx_x86
    .globl  store_idx
    .type   store_idx, @function
store_idx:
.LFB1:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movq    %rdi, -8(%rbp)
    movq    %rsi, -16(%rbp)
    movq    -8(%rbp), %rax
    movq    -16(%rbp), %rdx
    movq    %rdx, (%rax)
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE1:
    .size   store_idx, .-store_idx
    .globl  load_idx_x86
    .type   load_idx_x86, @function
load_idx_x86:
.LFB2:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movq    %rdi, -8(%rbp)
    movq    -8(%rbp), %rax
    movq    (%rax), %rax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE2:
    .size   load_idx_x86, .-load_idx_x86
    .globl  load_idx
    .type   load_idx, @function
load_idx:
.LFB3:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movq    %rdi, -8(%rbp)
    movq    -8(%rbp), %rax
    movq    (%rax), %rax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE3:
    .size   load_idx, .-load_idx
    .ident  "GCC: (GNU) 4.8.2 20140120 (Red Hat 4.8.2-16)"
    .section    .note.GNU-stack,"",@progbits

对于80x86,CPU保证对齐的加载和存储是原子性的。如果编译器可以保证加载/存储始终正确对齐,则上面所看到的代码就没问题了。然而... - Brendan
3
我并不完全相信编译器能够保证加载/存储总是正确对齐的(例如(uint64_t*)&myArrayOfChar[3]),因此我不认为这是安全的;除非你滥用"实现定义"(关于指针类型转换)作为一个"意外但在技术上允许摧毁你整整一周"行为的可怜借口(这似乎是GCC开发者变得喜欢的东西)。 - Brendan
dest从uint64_t更改为void可能会很有趣,在这种情况下,编译器可能不应该假定dest已对齐。 - Timothy Johns
__atomic_不接受void或structs。 - Bigtree
原子性部分的解释可以在 https://dev59.com/i1oV5IYBdhLWcg3wJ8Ab 找到。但这并不是完全重复,因为您还询问了关于栅栏的问题,而这里的答案也正确地回答了这个问题。另请参阅 http://preshing.com/20120930/weak-vs-strong-memory-models/。 - Peter Cordes
@brendan - 编译器一直假定内存正确对齐,各种别名和转换规则都是为了确保它被保留。因此,在这方面,GCC并没有做其他编译器一直在做的事情。特别是,如果编译器不假定对齐,你几乎无法在不允许错误对齐内存操作的平台上生成高效的代码。虽然它们今天不太常见,但它们存在,并且历史上有很多,因此假定对齐一直非常重要。 - BeeOnRope
1个回答

2
为什么store_idx_x86()的汇编输出与store_idx()相同,load_idx_x86()的汇编输出与load_idx()相同?
在x86上,假设编译器强制对齐,它们是相同的操作。对于本机大小或更小的对齐地址进行的加载和存储操作保证是原子性的。参考Intel manual vol 3A, 8.1.1
“Pentium处理器(以及后来的处理器)保证以下附加内存操作将始终以原子方式执行:读取或写入在64位边界上对齐的四元组字...”
此外,x86实施了强有序内存模型,这意味着每个存储和加载都具有隐式释放和获取语义。
最后,你提到的栅栏指令只有在使用英特尔的非暂态SSE指令(这里有一个很好的参考)或需要创建存储-加载栅栏(这篇文章)时才需要(而那个实际上是mfencelock指令)。
另外:我对英特尔手册中的那个声明感到好奇,所以我设计了一个测试程序。令人沮丧的是,在我的电脑上(2核心i3-4030U),我得到了这个输出:
unaligned
4265292 / 303932066 | 1.40337%
unaligned, but in same cache line
2373 / 246957659 | 0.000960893%
aligned (8 byte)
0 / 247097496 | 0%

这似乎违反了英特尔的说法。我会调查一下。与此同时,您应该克隆那个演示程序并看看它给出了什么。在Linux上,您只需要使用-std=c++11 ... -pthread

作为对此的跟进,请查看我的博客文章:https://relativebit.com/2015/04/07/cpp-unaligned-memory-accesses.html - Myles Hathcock
以上评论的更新链接:https://hathcock.sh/2015/06/17/torn_reads.html - Myles Hathcock
1
你是否解决了关于缓存行内非对齐测试的问题?你的链接已经失效了。可能是因为英特尔手册是正确的,而你犯了一个错误,因为它们仍然说,如果不跨越缓存行边界,则访问是原子性的。 :P(仅适用于cached访问,即在写回内存上,不适用于uncacheable,但你必须费尽心思才能映射一些设备内存或VGA内存。) - Peter Cordes

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