在x86/64(Intel/AMD 64位)处理器上,在字边界上对齐的变量是否比未对齐的加载操作更快?
我的一位同事辩称,未对齐的载荷很慢,应该避免.他引用了项目填充到结构中的单词边界,作为未对齐加载缓慢的证明.例:
struct A {
char a;
uint64_t b;
};
Run Code Online (Sandbox Code Playgroud)
结构A通常大小为16个字节.
另一方面,Snappy压缩器的文档指出Snappy假设"未对齐的32位和64位加载和存储很便宜".根据源代码,英特尔32和64位处理器也是如此.
那么:这里的真相是什么?如果和未对齐的载荷减少多少?在哪种情况下?
许多低延迟开发指南讨论了在特定地址边界上对齐内存分配:
https://github.com/real-logic/simple-binary-encoding/wiki/Design-Principles#word-aligned-access
http://www.alexonlinux.com/aligned-vs-unaligned-memory-access
但是,第二个链接是从2008年开始的.在2019年,在地址边界上调整内存是否仍能提高英特尔CPU的性能?我认为英特尔CPU不再会因访问未对齐地址而导致延迟损失?如果没有,在什么情况下应该这样做?我应该对齐每个堆栈变量吗?类成员变量?
有没有人有任何例子表明他们在调整内存方面取得了显着的性能提升?
当-O3使用GCC 4.8/4.9/5.1 编译程序时,我无法确定段错误的原因.对于GCC 4.9.x,我在Cygwin,Debian 8(x64)和Fedora 21(x64)上看过它.其他人在GCC 4.8和5.1上体验过它.
该程序-O2很好,其他版本的GCC很好,在其他编译器(如MSVC,ICC和Clang)下很好.
下面是GDB下的崩溃,但没有任何事情发生在我身上.源代码misc.cpp:26如下,但它是一个简单的异或:
((word64*)buf)[i] ^= ((word64*)mask)[i];
Run Code Online (Sandbox Code Playgroud)
有问题的代码在演员表之前检查64位字对齐.从反汇编中-O3,我知道它与vmovdqa指令有关:
(gdb) disass 0x0000000000539fc3
...
0x0000000000539fbc <+220>: vxorps 0x0(%r13,%r10,1),%ymm0,%ymm0
=> 0x0000000000539fc3 <+227>: vmovdqa %ymm0,0x0(%r13,%r10,1)
0x0000000000539fca <+234>: add $0x20,%r10
Run Code Online (Sandbox Code Playgroud)
看起来GCC正在使用SSE向量-O3,而不是使用它们-O2.(感谢亚历杭德罗提出的建议).
我会天真地问:vmovdqa对齐要求是否大于64位字?是这样,为什么GCC在单词不是128位对齐时选择它?
是什么导致了这里的段错误?我该如何进一步排除故障?
另请参阅错误66852 - 在64位对齐阵列上发出的vmovdqa指令,导致segfault.它是针对这个问题提交的,所以目前尚未证实.
$ gdb ./cryptest.exe
GNU gdb (Debian 7.7.1+dfsg-5) 7.7.1
...
(gdb) r v
...
Testing MessageDigest algorithm SHA-3-224.
.....
Program received signal SIGSEGV, …Run Code Online (Sandbox Code Playgroud) 我有一个相当简单的循环:
auto indexRecord = getRowPointer(0);
bool equals;
// recordCount is about 6 000 000
for (int i = 0; i < recordCount; ++i) {
equals = BitString::equals(SelectMask, indexRecord, maxBytesValue);
rowsFound += equals;
indexRecord += byteSize; // byteSize is 7
}
Run Code Online (Sandbox Code Playgroud)
哪里BitString::equals:
static inline bool equals(const char * mask, const char * record, uint64_t maxVal) {
return !(((*( uint64_t * ) mask) & (maxVal & *( uint64_t * ) record)) ^ (maxVal & *( uint64_t * ) record));
} …Run Code Online (Sandbox Code Playgroud) 鉴于此代码片段
#include <cstdint>
#include <cstddef>
struct Data {
uint64_t a;
//uint64_t b;
};
void foo(
void* __restrict data_out,
uint64_t* __restrict count_out,
std::byte* __restrict data_in,
uint64_t count_in)
{
for(uint64_t i = 0; i < count_in; ++i) {
Data value = *reinterpret_cast<Data* __restrict>(data_in + sizeof(Data) * i);
static_cast<Data* __restrict>(data_out)[(*count_out)++] = value;
}
}
Run Code Online (Sandbox Code Playgroud)
clang 用 memcpy 调用替换循环foo,正如预期的那样 ( godbolt ),给出 Rpass 输出:
example.cpp:16:59: remark: Formed a call to llvm.memcpy.p0.p0.i64() intrinsic from load and store instruction in _Z3fooPvPmPSt4bytem function [-Rpass=loop-idiom]
static_cast<Data* …Run Code Online (Sandbox Code Playgroud) 鉴于:
#include <string.h>
bool test_data(void *data)
{
return memcmp(data, "abcd", 4) == 0;
}
Run Code Online (Sandbox Code Playgroud)
编译器可以将其优化为:
test_data:
cmpl $1684234849, (%rdi)
sete %al
ret
Run Code Online (Sandbox Code Playgroud)
这很好。
但如果我使用我自己的memcmp()(而不是来自<string.h>),编译器无法将其优化为单个cmpl指令。相反,它这样做:
test_data:
cmpl $1684234849, (%rdi)
sete %al
ret
Run Code Online (Sandbox Code Playgroud)
test_data:
cmpb $97, (%rdi)
jne .L5
cmpb $98, 1(%rdi)
jne .L5
cmpb $99, 2(%rdi)
jne .L5
cmpb $100, 3(%rdi)
sete %al
ret
.L5:
xorl %eax, %eax
ret
Run Code Online (Sandbox Code Playgroud)
链接: https: //godbolt.org/z/Kfhchr45a
根据cppreference,硬件可能要求 an 引用的对象atomic_ref<T>比其他T对象具有更严格的对齐方式,并且 an 上的操作是否atomic_ref是无锁的可以取决于引用对象的对齐方式。
为什么只需要引用的对象具有atomic_ref适当的对齐方式,而std::atomic似乎没有强加这一要求?
在低级语言中,有可能mov是第一个数组元素的双字(32位),它将溢出写入第二个,第三个和第四个元素,或者mov一个字(16位)到第一个,它将溢出到第二个元件.
如何在c中实现同样的效果?当试图例如:
char txt[] = {0, 0};
txt[0] = 0x4142;
Run Code Online (Sandbox Code Playgroud)
它发出警告 [-Woverflow]
并且值txt[1] 不会改变并txt[0]设置为0x42.
如何获得与汇编相同的行为:
mov word [txt], 0x4142
先前的汇编指令将设置在第一元件[txt+0]到0x42第二元件[txt+1]到0x41.
这个建议怎么样?
将数组定义为单个变量.
uint16_t txt;
txt = 0x4142;
Run Code Online (Sandbox Code Playgroud)
并使用((uint8_t*) &txt)[0]第一个元素和((uint8_t*) &txt)[1]第二个元素访问元素.
我最近研究了一个用GCC 8编译的软件中的segfault。代码如下(这只是一个草图)
struct Point
{
int64_t x, y;
};
struct Edge
{
// some other fields
// ...
Point p; // <- at offset `0xC0`
Edge(const Point &p) p(p) {}
};
Edge *create_edge(const Point &p)
{
void *raw_memory = my_custom_allocator(sizeof(Edge));
return new (raw_memory) Edge(p);
}
Run Code Online (Sandbox Code Playgroud)
这里的关键点是my_custom_allocator()返回指向未对齐内存的指针。代码崩溃是因为为了将原始点复制p到Edge::p新对象的字段中,编译器在 [内联] 构造函数代码中使用了movdqu/movaps对
movdqu 0x0(%rbp), %xmm1 ; read the original object at `rbp`
...
movaps %xmm1, 0xc0(%rbx) ; store it into the new `Edge` object …Run Code Online (Sandbox Code Playgroud) 我们看一下代码
#include <stdint.h>
#pragma pack (push,1)
typedef struct test_s
{
uint64_t a1;
uint64_t a2;
uint64_t a3;
uint64_t a4;
uint64_t a5;
uint64_t a6;
uint64_t a7;
uint8_t b1;
uint64_t a8;
}test;
int main()
{
test t;
__atomic_store_n(&(t.a8), 1, __ATOMIC_RELAXED);
}
Run Code Online (Sandbox Code Playgroud)
由于我们有打包结构,a8 不是自然对齐的,也应该在不同的 64 字节缓存边界之间分割,但生成的程序集 GCC 12.2 是
main:
push rbp
mov rbp, rsp
mov eax, 1
mov QWORD PTR [rbp-23], rax
mov eax, 0
pop rbp
ret
Run Code Online (Sandbox Code Playgroud)
为什么它翻译成简单的MOV?在这种情况下 MOV 不是原子的吗?
添加:clang 16 调用原子函数的相同代码并转换为
main: # @main
push rbp
mov rbp, rsp
sub …Run Code Online (Sandbox Code Playgroud)