std::vector 在<Simd_wrapper>内存中有连续的数据吗?

Does std::vector<Simd_wrapper> have contiguous data in memory?

本文关键字:连续 数据 内存 wrapper vector lt Simd std gt      更新时间:2023-10-16
class Wrapper {
public:
    // some functions operating on the value_
    __m128i value_;
};
int main() {
    std::vector<Wrapper> a;
    a.resize(100);
}

vector aWrapper对象的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。 正如其他答案所指出的那样,那里也可能有填充,这不能保证在编译器之间甚至同一编译器的不同版本之间保持一致。

最后,假设它将在给定的编译器上连续可能是不值得的,即使您测试它并且它可以工作,像稍后添加虚拟方法这样的简单事情也会让您头疼。