std::vector 在<Simd_wrapper>内存中有连续的数据吗?
Does std::vector<Simd_wrapper> have contiguous data in memory?
class Wrapper {
public:
// some functions operating on the value_
__m128i value_;
};
int main() {
std::vector<Wrapper> a;
a.resize(100);
}
vector a
中Wrapper
对象的value_
属性是否总是占用连续内存,__m128i values
之间没有任何间隙?
我的意思是:
[128 bit for 1st Wrapper][no gap here][128bit for 2nd Wrapper] ...
到目前为止,对于 g++ 和我正在使用的英特尔 CPU 以及 gcc godbolt,这似乎是正确的。
由于 Wrapper
对象中只有一个 __m128i 属性,这是否意味着编译器始终不需要在内存中添加任何类型的填充?(POD 对象矢量的内存布局(
测试代码 1:
#include <iostream>
#include <vector>
#include <x86intrin.h>
int main()
{
static constexpr size_t N = 1000;
std::vector<__m128i> a;
a.resize(1000);
//__m128i a[1000];
uint32_t* ptr_a = reinterpret_cast<uint32_t*>(a.data());
for (size_t i = 0; i < 4*N; ++i)
ptr_a[i] = i;
for (size_t i = 1; i < N; ++i){
a[i-1] = _mm_and_si128 (a[i], a[i-1]);
}
for (size_t i = 0; i < 4*N; ++i)
std::cout << ptr_a[i];
}
警告:
warning: ignoring attributes on template argument
'__m128i {aka __vector(2) long long int}'
[-Wignored-attributes]
组装(GCC神螺栓(:
.L9:
add rax, 16
movdqa xmm1, XMMWORD PTR [rax]
pand xmm0, xmm1
movaps XMMWORD PTR [rax-16], xmm0
cmp rax, rdx
movdqa xmm0, xmm1
jne .L9
我想这意味着数据是连续的,因为循环只是在循环的每个周期中读取的内存地址上增加 16 个字节。它使用pand
来执行按位和。
测试代码 2:
#include <iostream>
#include <vector>
#include <x86intrin.h>
class Wrapper {
public:
__m128i value_;
inline Wrapper& operator &= (const Wrapper& rhs)
{
value_ = _mm_and_si128(value_, rhs.value_);
}
}; // Wrapper
int main()
{
static constexpr size_t N = 1000;
std::vector<Wrapper> a;
a.resize(N);
//__m128i a[1000];
uint32_t* ptr_a = reinterpret_cast<uint32_t*>(a.data());
for (size_t i = 0; i < 4*N; ++i) ptr_a[i] = i;
for (size_t i = 1; i < N; ++i){
a[i-1] &=a[i];
//std::cout << ptr_a[i];
}
for (size_t i = 0; i < 4*N; ++i)
std::cout << ptr_a[i];
}
组装(GCC神螺栓(
.L9:
add rdx, 2
add rax, 32
movdqa xmm1, XMMWORD PTR [rax-16]
pand xmm0, xmm1
movaps XMMWORD PTR [rax-32], xmm0
movdqa xmm0, XMMWORD PTR [rax]
pand xmm1, xmm0
movaps XMMWORD PTR [rax-16], xmm1
cmp rdx, 999
jne .L9
看起来也没有填充。 rax
每一步增加 32,即 2 x 16。这种额外的add rdx,2
肯定不如测试代码 1 的循环。
测试自动矢量化
#include <iostream>
#include <vector>
#include <x86intrin.h>
int main()
{
static constexpr size_t N = 1000;
std::vector<__m128i> a;
a.resize(1000);
//__m128i a[1000];
uint32_t* ptr_a = reinterpret_cast<uint32_t*>(a.data());
for (size_t i = 0; i < 4*N; ++i)
ptr_a[i] = i;
for (size_t i = 1; i < N; ++i){
a[i-1] = _mm_and_si128 (a[i], a[i-1]);
}
for (size_t i = 0; i < 4*N; ++i)
std::cout << ptr_a[i];
}
装配(神螺栓(:
.L21:
movdqu xmm0, XMMWORD PTR [r10+rax]
add rdi, 1
pand xmm0, XMMWORD PTR [r8+rax]
movaps XMMWORD PTR [r8+rax], xmm0
add rax, 16
cmp rsi, rdi
ja .L21
。我只是不知道这是否总是适用于英特尔 cpu 和 g++/英特尔 c++ 编译器/(在此处插入编译器名称(......
不能保证class Wrapper
末尾不会有填充,只是在开头不会有填充。
根据C++11
标准:
9.2 类成员 [ 类.mem ]
20 指向标准布局结构对象的指针,使用reinterpret_cast适当转换,指向其初始成员(或者,如果该成员是位字段,则指向它所在的单元(,反之亦然。[注意:因此,标准布局结构对象中可能存在未命名的填充,但不在其开头,这是实现适当对齐所必需的。
同样在sizeof
:
5.3.3 尺寸 [ expr.sizeof ]
2 应用于引用或引用类型时,结果是引用类型的大小。应用时 对于类,结果是该类的对象中的字节数,包括 将该类型的对象放在数组中。
安全地假设无填充,除非您针对非标准 ABI 进行编译。
所有针对相同 ABI 的编译器必须对结构/类大小/布局做出相同的选择,并且所有标准 ABI/调用约定在您的结构中都没有填充。 (即x86-32和x86-64 System V和Windows,有关链接,请参阅x86标签wiki(。 使用一个编译器进行的实验可确认针对同一平台/ABI 的所有编译器。
请注意,这个问题的范围仅限于支持英特尔内部函数和__m128i
类型的 x86 编译器,这意味着我们比仅从 ISO C++ 标准中获得的没有任何特定于实现的内容要强大得多。
正如@zneak指出的那样,您可以在类 def 中static_assert(std::is_standard_layout<Wrapper>::value)
提醒人们不要添加任何虚拟方法,这将为每个实例添加一个 vtable 指针。
不能保证。 Galik的回答引用了标准,所以我将专注于假设它是连续的一些风险。
我写了这个小程序并使用gcc编译,它确实将整数连续放置:
#include <iostream>
#include <vector>
class A
{
public:
int a;
int method() { return 1;}
float method2() { return 5.5; }
};
int main()
{
std::vector<A> as;
for(int i = 0; i < 10; i++)
{
as.push_back(A());
}
for(int i = 0; i < 10; i++)
{
std::cout << &as[i] << std::endl;
}
}
然而,通过一个小的变化,差距开始出现:
#include <iostream>
#include <vector>
class A
{
public:
int a;
int method() { return 1;}
float method2() { return 5.5; }
virtual double method3() { return 0.1; } //this is the only change
};
int main()
{
std::vector<A> as;
for(int i = 0; i < 10; i++)
{
as.push_back(A());
}
for(int i = 0; i < 10; i++)
{
std::cout << &as[i] << std::endl;
}
}
具有虚拟方法的对象(或从具有虚拟方法的对象继承的对象(需要存储一些额外的信息,以了解在哪里可以找到合适的方法,因为它在运行时之前不知道基类或任何重写之间的哪个。 这就是为什么建议永远不要在类上使用memset
。 正如其他答案所指出的那样,那里也可能有填充,这不能保证在编译器之间甚至同一编译器的不同版本之间保持一致。
最后,假设它将在给定的编译器上连续可能是不值得的,即使您测试它并且它可以工作,像稍后添加虚拟方法这样的简单事情也会让您头疼。
- C++,您能否设计一种数据结构,将指针保存在连续内存中并且不会使它们失效?
- 将数据从 c++ 文件连续发送到 Python 脚本以进行进一步处理
- 我可以使用哪种数据结构来释放连续内存中的内存?
- 我无法从串行端口发送连续数据
- 将不相邻的内存缓冲区视为连续缓冲区的数据结构
- 在编译时检查特征类型保存的数据在内存中是否连续
- 无法找到"断管"错误的原因,同时通过Beast websocket发送连续数据块
- boost asio async_receive_from()在连续发送帧时缺少udp帧数据检索
- 从QTcpSocket上的数据流中连续运行复杂算法的最佳Qt线程解决方案是什么
- RandomAccessIterator是否意味着数据在内存中是连续的
- 与GPS的串行通信-连续数据传输
- std::vector中的数据存储是否连续
- 明确预摘要非连续数据
- 一种按排序顺序保持元素的数据结构,支持快速插入和计算连续元素之间的最大差异
- 如何从特征矩阵中获取不连续的数据"block"?
- 递归类型真的是构建不连续的任意大小数据结构的唯一方法吗
- 在QTcpSocket中连续发送XML数据的最简单方法
- 在整数数据类型和连续两个字符类型之后.第 2 个字符的数据类型跳过..为什么
- 如何在庞大的二进制数据中快速识别 1(索引)的连续范围
- 将char***作为连续数据复制到char*变量