【问题标题】:Does std::vector<Simd_wrapper> have contiguous data in memory?std::vector<Simd_wrapper> 在内存中有连续的数据吗?
【发布时间】:2017-03-21 10:31:44
【问题描述】:
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++ 和我正在使用的 Intel cpu 以及 gcc godbolt 似乎都是如此。

由于Wrapper 对象中只有一个 __m128i 属性,这是否意味着编译器总是不需要在内存中添加任何类型的填充? (Memory layout of vector of POD objects)

测试代码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 god bolt):

.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 god bolt)

.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];
}

大会 (god bolt):

.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

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

【问题讨论】:

  • 可以,但不保证对齐正确
  • 它是 __m128i 元素。所以我希望这意味着向量的每个元素都有 128 位对齐。
  • 那是另一个问题。保证连续存储,不保证过度对齐。
  • 不是答案,但您可以(并且应该)静态地断言这些事情。然后,如果这些断言失败,您可以使用特定于实现(或标准,如果存在)的方法来修复结构。
  • @marshalcraft,标准保证向量是连续的。

标签: c++ vector simd


【解决方案1】:

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

根据C++11标准:

9.2 类成员[ class.mem ]

20 指向标准布局结构对象的指针,使用 reinterpret_cast 适当转换,指向其初始成员(或者如果该成员是位字段,则指向它所在的单元驻留),反之亦然。 [注意:因此,标准布局结构对象中可能有未命名的填充,但不是在其开头,这是实现适当对齐所必需的。 ——尾注]

也在sizeof下:

5.3.3 Sizeof [ expr.sizeof ]

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

【讨论】:

  • 出于好奇:具有单个成员且大小为 2 的幂的数据结构是否有可能现实有任何填充?
  • @MikeMB 我对CPU 架构的研究还不够多,无法冒险猜测。
  • @MikeMB,我看不到这种情况发生。结构被填充,这样如果你把它们放在一个数组中,结构的第一个元素就会对齐。多余的填充是不必要的。
【解决方案2】:

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

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

【讨论】:

  • A standard layout class 和 OP 一样,不能有 vtable。这可以在编译时使用static_assert(std::is_standard_layout&lt;Wrapper&gt;::value) 进行验证。
  • @zneak 我的意思是,虽然现在它可能是可以预测的,但以后很容易忘记并搞砸。如果您确实使用static_assert 来防止在它发生更改时进行编译,那么您就知道何时必须解决问题。但是现在不依赖它是真的,然后在事情发生变化时必须修复它不是更容易吗?
  • 我会说不是。 OP 可能会问,因为他想将该数据传递给用汇编编写的函数(这对于 SIMD 计算仍然相对常见),但当他不处于紧密循环中时,仍然可以方便地使用单个元素的方法。
【解决方案3】:

在实践中可以安全地假设无填充,除非您正在为非标准 ABI 进行编译。

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

请注意,此问题的范围仅限于支持 Intel 内在函数和 __m128i 类型的 x86 编译器,这意味着与您从没有任何特定于实现的东西的 ISO C++ 标准中获得的相比,我们有更强有力的保证。


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

【讨论】:

    猜你喜欢
    • 2011-09-21
    • 2014-03-27
    • 2016-04-17
    • 2014-08-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-10-23
    相关资源
    最近更新 更多