std :: vector <simd_wrapper>在内存中是否有连续的数据?

时间:2016-11-07 22:46:50

标签: c++ vector simd

class Wrapper {
public:
    // some functions operating on the value_
    __m128i value_;
};

int main() {
    std::vector<Wrapper> a;
    a.resize(100);
}

value_Wrappervector a对象的__m128i values属性是否总是占据连续的记忆而[128 bit for 1st Wrapper][no gap here][128bit for 2nd Wrapper] ... 之间没有任何差距?

我的意思是:

Wrapper

到目前为止,g ++和我使用的英特尔cpu以及gcc godbolt似乎都是如此。

由于#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]; } 对象中只有一个__m128i属性,这是否意味着编译器总是不需要在内存中添加任何类型的填充? (Memory layout of vector of POD objects

测试代码1:

warning: ignoring attributes on template argument 
'__m128i {aka __vector(2) long long int}'
[-Wignored-attributes]

警告:

.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

汇编(gcc god bolt):

pand

我想这意味着数据是连续的,因为循环只是将16个字节添加到它在循环的每个循环中读取的内存地址。它使用#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]; } 来执行按位和。

测试代码2:

.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

大会(gcc god bolt

rax

看起来也没有填充。 add rdx,2每步增加32,即2 x 16.额外的#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]; } 绝对不如测试代码1的循环好。

测试自动矢量化

.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

汇编(god bolt):

let data: Int = json["data"] as? Int ?? 0

...我只是不知道对于intel cpu和g ++ / intel c ++编译器/(在此处插入编译器名称)是否总是如此...

3 个答案:

答案 0 :(得分:2)

不能保证在class Wrapper的末尾不会有填充,只是在开头处没有填充。

根据C++11标准:

  

9.2 班级成员 [ class.mem ]

     

20 指向标准布局结构对象的指针,使用reinterpret_cast进行适当转换,指向其初始成员(或者如果该成员是位字段,则指向它的单位)居住),反之亦然。 [注意:因此,在标准布局结构对象中可能存在未命名的填充,但不是在其开头,以实现适当的对齐。 - 结束说明]

也在sizeof下:

  

5.3.3 尺寸 [ expr.sizeof ]

     

2 当应用于引用或引用类型时,结果是引用类型的大小。申请时   对于一个类,结果是该类对象中的字节数,包括所需的任何填充   将该类型的对象放在数组中。

答案 1 :(得分:1)

不保证。 Galik's answer引用了标准,因此我将重点关注假设它将是连续的一些风险。

我写了这个小程序并用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; 
  }
}

具有虚方法的对象(或从具有虚方法的对象继承的对象)需要存储一些额外的信息以知道在哪里找到适当的方法,因为它不知道基类或任何基类之间的哪个覆盖直到运行时。这就是为什么建议never use memset on a class。正如其他答案所指出的那样,也可能存在填充,这不能保证在编译器甚至同一编译器的不同版本之间保持一致。

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

答案 2 :(得分:1)

在实践中假设填充是安全的,除非您正在编译非标准ABI。

所有针对相同ABI的编译器必须对结构/类大小/布局做出相同的选择,并且所有标准ABI /调用约定在结构中都没有填充。 (即x86-32和x86-64 System V和Windows,请参阅标记wiki以获取链接)。您使用一个编译器的实验确认了针对同一平台/ ABI的所有编译器。

请注意,此问题的范围仅限于支持英特尔内在函数和__m128i类型的x86编译器,这意味着我们拥有比仅使用ISO C ++标准所获得的更强大的保证。任何特定于实现的东西。

正如@zneak指出的那样,你可以在类def中static_assert(std::is_standard_layout<Wrapper>::value)来提醒人们不要添加任何虚拟方法,这会为每个实例添加一个vtable指针。