SIMD 运算
SIMD(Single Instruction Multiple Data)即单指令流多数据流,是一种采用一个控制器来控制多个处理器,同时对一组数据(又称“数据向量”)中的每一个分别执行相同的操作从而实现空间上的并行性的技术。简单来说就是一个指令能够同时处理多个数据。
SSE/AVX指令主要定义于以下一些头文件中:
: SSE, 支持同时对4个32位单精度浮点数的操作。 : SSE 2, 支持同时对2个64位双精度浮点数的操作。 : SSE 3, 支持对SIMD寄存器的水平操作(horizontal operation),如hadd, hsub等...。 : SSSE 3, 增加了额外的instructions。 : SSE 4.1, 支持点乘以及更多的整形操作。 : SSE 4.2, 增加了额外的instructions。 : AVX, 支持同时操作8个单精度浮点数或4个双精度浮点数。
一步一步使用simd
一个简单的例子
#include <iostream>
#include <immintrin.h>
int main(int argc, const char** argv)
{
double input1[4] = {1, 1, 1, 1};
double input2[4] = {1, 2, 3, 4};
double result[4];
std::cout << "address of input1: " << input1 << std::endl;
std::cout << "address of input2: " << input2 << std::endl;
__m256d a = _mm256_load_pd(input1);
__m256d b = _mm256_load_pd(input2);
__m256d c = _mm256_add_pd(a, b);
_mm256_store_pd(result, c);
std::cout << result[0] << " " << result[1] << " " << result[2] << " " << result[3] << std::endl;
return 0;
}
使用g++编译运行.
g++ -std=c++11 -march=native main.cpp -o simd
在PC上默认参数可以看到是\(march=x86\_64\).
g++ -Q --help=target
发现会出现crash。这里使用了AVX指令集运算,要求32字节对齐,我们可以把这两个输入变量的地址除以32,看是否能够整除。结果发现此时的地址都不能整除。当然,其实直接看倒数第二位是否是偶数即可,是偶数就可以被32整除,是奇数则不能被32整除。
对于局部变量内存地址是编译器决定的,需要告诉编译器声明变量需要内存对齐,gcc的语法为__attribute__((aligned(32))),MSVC的语法为 __declspec(align(32))。
__attribute__ ((aligned (32))) double input1[4] = {1, 1, 1, 1};
__attribute__ ((aligned (32))) double input2[4] = {1, 2, 3, 4};
__attribute__ ((aligned (32))) double result[4];
此时编译运行,发现程序可以正常运行,并且内存地址都是32字节对齐。
封装
Eigen中会将向量封装起来,此时可以仿照封装一个向量加法。
#include <immintrin.h>
#include <iostream>
class Vector4d {
using aligned_double4 = __attribute__ ((aligned (32))) double[4];
public:
Vector4d(double d1, double d2, double d3, double d4)
{
data[0] = d1;
data[1] = d2;
data[2] = d3;
data[3] = d4;
std::cout << "address: " << data << std::endl;
}
aligned_double4 data;
};
Vector4d operator+ (const Vector4d& v1, const Vector4d& v2)
{
__m256d data1 = _mm256_load_pd(v1.data);
__m256d data2 = _mm256_load_pd(v2.data);
__m256d data3 = _mm256_add_pd(data1, data2);
Vector4d result = {0, 0, 0, 0};
_mm256_store_pd(result.data, data3);
return result;
}
std::ostream& operator<< (std::ostream& o, const Vector4d& v)
{
o << "(" << v.data[0] << ", " << v.data[1] << ", " << v.data[2] << ", " << v.data[3] << ")";
return o;
}
int main(int argc, const char** argv)
{
Vector4d input1 = {1, 1, 1, 1};
Vector4d input2 = {1, 2, 3, 4};
Vector4d result = input1 + input2;
std::cout << result << std::endl;
return 0;
}
此时程序正常运行,但是当我们动态实例化对象就会发现内存对齐出错了,此时程序crash。
int main(int argc, const char** argv)
{
/*Vector4d input1 = {1, 1, 1, 1};
Vector4d input2 = {1, 2, 3, 4};
Vector4d result = input1 + input2;*/
Vector4d* input1 = new Vector4d{1, 1, 1, 1};
Vector4d* input2 = new Vector4d{1, 2, 3, 4};
Vector4d result = *input1 + *input2;
std::cout << result << std::endl;
std::cout << result << std::endl;
return 0;
}
问题的根源在于直接声明的对象是存储在栈上的,其内存地址由编译器在编译时确定,因此预编译指令会生效。但用new动态创建的对象则存储在堆中,其地址在运行时确定。C++的运行时库并不会关心预编译指令声明的对齐方式,需要重载new方法。
void* operator new (std::size_t count) {
void* original = ::operator new(count + 32);
void* aligned = reinterpret_cast<void*>((reinterpret_cast<size_t>(original) & ~size_t(32 - 1)) + 32);
*(reinterpret_cast<void**>(aligned) - 1) = original;
return aligned;
}
void operator delete (void* ptr) {
::operator delete(*(reinterpret_cast<void**>(ptr) - 1));
}
此时发现编译运行,程序又正常运行。
作为成员变量
当我们需要Vector4d作为成员变量的时候,bug又出现了,内存又出现了不对齐的情况。
class Point {
public:
Point(const Vector4d& position) : position(position) {}
Vector4d position;
};
int main(int argc, const char** argv)
{
Vector4d* input1 = new Vector4d{1, 1, 1, 1};
Vector4d* input2 = new Vector4d{1, 2, 3, 4};
Point* point1 = new Point{*input1};
Point* point2 = new Point{*input2};
Vector4d result = point1->position + point2->position;
std::cout << result << std::endl;
delete input1;
delete input2;
delete point1;
delete point2;
return 0;
}
此时需要在Point类中同样重载new和delete方法,将次操作封装为宏,方便其他地方调用。
#define ALIGNED_OPERATOR_NEW \
void* operator new (std::size_t count) { \
void* original = ::operator new(count + 32); \
void* aligned = reinterpret_cast<void*>((reinterpret_cast<size_t>(original) & ~size_t(32 - 1)) + 32); \
*(reinterpret_cast<void**>(aligned) - 1) = original; \
return aligned;\
} \
void operator delete (void* ptr) { \
::operator delete(*(reinterpret_cast<void**>(ptr) - 1)); \
}
完整程序如下:
#include <immintrin.h>
#include <iostream>
#define ALIGNED_OPERATOR_NEW \
void* operator new (std::size_t count) { \
void* original = ::operator new(count + 32); \
void* aligned = reinterpret_cast<void*>((reinterpret_cast<size_t>(original) & ~size_t(32 - 1)) + 32); \
*(reinterpret_cast<void**>(aligned) - 1) = original; \
return aligned;\
} \
void operator delete (void* ptr) { \
::operator delete(*(reinterpret_cast<void**>(ptr) - 1)); \
}
class Vector4d {
using aligned_double4 = __attribute__ ((aligned (32))) double[4];
public:
ALIGNED_OPERATOR_NEW
Vector4d(double d1, double d2, double d3, double d4)
{
data[0] = d1;
data[1] = d2;
data[2] = d3;
data[3] = d4;
std::cout << "address: " << data << std::endl;
}
aligned_double4 data;
};
Vector4d operator+ (const Vector4d& v1, const Vector4d& v2)
{
__m256d data1 = _mm256_load_pd(v1.data);
__m256d data2 = _mm256_load_pd(v2.data);
__m256d data3 = _mm256_add_pd(data1, data2);
Vector4d result = {0, 0, 0, 0};
_mm256_store_pd(result.data, data3);
return result;
}
std::ostream& operator<< (std::ostream& o, const Vector4d& v)
{
o << "(" << v.data[0] << ", " << v.data[1] << ", " << v.data[2] << ", " << v.data[3] << ")";
return o;
}
class Point {
public:
ALIGNED_OPERATOR_NEW
Point(const Vector4d& position) : position(position) {}
Vector4d position;
};
int main(int argc, const char** argv)
{
/*Vector4d input1 = {1, 1, 1, 1};
Vector4d input2 = {1, 2, 3, 4};
Vector4d result = input1 + input2;*/
/*Vector4d* input1 = new Vector4d{1, 1, 1, 1};
Vector4d* input2 = new Vector4d{1, 2, 3, 4};
Vector4d result = *input1 + *input2;*/
Vector4d input1 = {1, 1, 1, 1};
Vector4d input2 = {1, 2, 3, 4};
Point* point1 = new Point{input1};
Point* point2 = new Point{input2};
Vector4d result = point1->position + point2->position;
std::cout << result << std::endl;
delete point1;
delete point2;
return 0;
}
Reference
[1] https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html#expand=18,1155,1286,450,2913,3836,6065,6065&text=_mm_unpacklo_ps
[2] https://en.cppreference.com/w/cpp/memory/new/operator_new
[3] https://zhuanlan.zhihu.com/p/93824687