我获取 int 数组的点积的内在函数比普通代码慢,我做错了什么?
my intrinsic function in getting the dot product of an int array is slower than the normal code, what am I doing wrong?
我正在尝试了解 intrinsic 以及如何正确利用和优化它,我决定实现一个功能来得到两个数组的点积作为学习的起点
我创建了两个函数来获取整数数组的点积 int
,其中一个以正常方式编码,您循环遍历两个数组的每个元素,然后对每个元素执行乘法,然后 add/accumulate/sum结果乘积得到点积。
另一种使用内在的方式是,我对每个数组的四个元素执行内在运算,我用 _mm_mullo_epi32
将它们中的每一个相乘,然后使用 2 水平加法 _mm_hadd_epi32
得到当前 4 个元素的总和,之后我将它加到 dot_product,然后继续下一个四个元素,然后重复直到我得到计算的限制 vec_loop
,然后我用正常的方式计算其他剩余的元素,避免计算出数组的内存,然后我比较两者的性能。
两种点积函数的头文件:
// main.hpp
#ifndef main_hpp
#define main_hpp
#include <iostream>
#include <immintrin.h>
template<typename T>
T scalar_dot(T* a, T* b, size_t len){
T dot_product = 0;
for(size_t i=0; i<len; ++i) dot_product += a[i]*b[i];
return dot_product;
}
int sse_int_dot(int* a, int* b, size_t len){
size_t vec_loop = len/4;
size_t non_vec = len%4;
size_t start_non_vec_i = len-non_vec;
int dot_prod = 0;
for(size_t i=0; i<vec_loop; ++i)
{
__m128i va = _mm_loadu_si128((__m128i*)(a+(i*4)));
__m128i vb = _mm_loadu_si128((__m128i*)(b+(i*4)));
va = _mm_mullo_epi32(va,vb);
va = _mm_hadd_epi32(va,va);
va = _mm_hadd_epi32(va,va);
dot_prod += _mm_cvtsi128_si32(va);
}
for(size_t i=start_non_vec_i; i<len; ++i) dot_prod += a[i]*b[i];
return dot_prod;
}
#endif
cpp代码测量每个函数所用时间
// main.cpp
#include <iostream>
#include <chrono>
#include <random>
#include "main.hpp"
int main()
{
// generate random integers
unsigned seed = std::chrono::steady_clock::now().time_since_epoch().count();
std::mt19937_64 rand_engine(seed);
std::mt19937_64 rand_engine2(seed/2);
std::uniform_int_distribution<int> random_number(0,9);
size_t LEN = 10000000;
int* a = new int[LEN];
int* b = new int[LEN];
for(size_t i=0; i<LEN; ++i)
{
a[i] = random_number(rand_engine);
b[i] = random_number(rand_engine2);
}
#ifdef SCALAR
int dot1 = 0;
#endif
#ifdef VECTOR
int dot2 = 0;
#endif
// timing
auto start = std::chrono::high_resolution_clock::now();
#ifdef SCALAR
dot1 = scalar_dot(a,b,LEN);
#endif
#ifdef VECTOR
dot2 = sse_int_dot(a,b,LEN);
#endif
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::nanoseconds>(end-start);
std::cout<<"proccess taken "<<duration.count()<<" nanoseconds\n";
#ifdef SCALAR
std::cout<<"\nScalar : Dot product = "<<dot1<<"\n";
#endif
#ifdef VECTOR
std::cout<<"\nVector : Dot product = "<<dot2<<"\n";
#endif
return 0;
}
编译:
- 内在版本:
g++ main.cpp -DVECTOR -msse4.1 -o main.o
- 普通版:
g++ main.cpp -DSCALAR -msse4.1 -o main.o
我的机器:
- 架构:x86_64
- CPU(s) : 1
- CPU 核心数:4
- 每个核心线程数:1
- 型号名称:Intel(R) Pentium(R) CPU N3700 @ 1.60GHz
- 一级缓存:96 KiB
- L1i 缓存:128 KiB
- 二级缓存:2 MiB
- 一些标志:sse、sse2、sse4_1、sse4_2
在 main.cpp
中有 10000000 个 int
数组的元素,当我编译上面的代码时在我的机器上,似乎内部函数 运行 比普通版本慢,大多数时候,内部函数需要大约 97529675 nanoseconds
有时甚至更长,而普通代码只需要大约 87568313 nanoseconds
,在这里我认为如果优化标志关闭,我的内部函数应该 运行 更快,但事实证明它确实有点慢。
所以我的问题是:
- 为什么我的内在函数 运行 比较慢? (我做错了什么吗?)
- 如何更正我的内在实现,正确的方法是什么?
- 编译器是否自动 vectorize/unroll 正常代码,即使优化标志关闭
- 根据我的机器规格,获取点积的最快方法是什么?
希望有人能帮忙,谢谢
所以根据 @Peter Cordes、@Qubit 和 @j6t 的建议,我稍微修改了代码,我现在只在循环内做乘法,然后我移动了水平在循环外添加...它设法将内部版本的性能从 97529675 nanoseconds
左右提高到 56444187 nanoseconds
左右,这比我以前的实现快得多,具有相同的编译标志和 10000000 个 int 数组元素。
这是 main.hpp
的新函数
int _sse_int_dot(int* a, int* b, size_t len){
size_t vec_loop = len/4;
size_t non_vec = len%4;
size_t start_non_vec_i = len-non_vec;
int dot_product;
__m128i vdot_product = _mm_set1_epi32(0);
for(size_t i=0; i<vec_loop; ++i)
{
__m128i va = _mm_loadu_si128((__m128i*)(a+(i*4)));
__m128i vb = _mm_loadu_si128((__m128i*)(b+(i*4)));
__m128i vc = _mm_mullo_epi32(va,vb);
vdot_product = _mm_add_epi32(vdot_product,vc);
}
vdot_product = _mm_hadd_epi32(vdot_product,vdot_product);
vdot_product = _mm_hadd_epi32(vdot_product,vdot_product);
dot_product = _mm_cvtsi128_si32(vdot_product);
for(size_t i=start_non_vec_i; i<len; ++i) dot_product += a[i]*b[i];
return dot_product;
}
如果此代码还有更多需要改进的地方,请指出,现在我将把它留在这里作为答案。
我正在尝试了解 intrinsic 以及如何正确利用和优化它,我决定实现一个功能来得到两个数组的点积作为学习的起点
我创建了两个函数来获取整数数组的点积 int
,其中一个以正常方式编码,您循环遍历两个数组的每个元素,然后对每个元素执行乘法,然后 add/accumulate/sum结果乘积得到点积。
另一种使用内在的方式是,我对每个数组的四个元素执行内在运算,我用 _mm_mullo_epi32
将它们中的每一个相乘,然后使用 2 水平加法 _mm_hadd_epi32
得到当前 4 个元素的总和,之后我将它加到 dot_product,然后继续下一个四个元素,然后重复直到我得到计算的限制 vec_loop
,然后我用正常的方式计算其他剩余的元素,避免计算出数组的内存,然后我比较两者的性能。
两种点积函数的头文件:
// main.hpp
#ifndef main_hpp
#define main_hpp
#include <iostream>
#include <immintrin.h>
template<typename T>
T scalar_dot(T* a, T* b, size_t len){
T dot_product = 0;
for(size_t i=0; i<len; ++i) dot_product += a[i]*b[i];
return dot_product;
}
int sse_int_dot(int* a, int* b, size_t len){
size_t vec_loop = len/4;
size_t non_vec = len%4;
size_t start_non_vec_i = len-non_vec;
int dot_prod = 0;
for(size_t i=0; i<vec_loop; ++i)
{
__m128i va = _mm_loadu_si128((__m128i*)(a+(i*4)));
__m128i vb = _mm_loadu_si128((__m128i*)(b+(i*4)));
va = _mm_mullo_epi32(va,vb);
va = _mm_hadd_epi32(va,va);
va = _mm_hadd_epi32(va,va);
dot_prod += _mm_cvtsi128_si32(va);
}
for(size_t i=start_non_vec_i; i<len; ++i) dot_prod += a[i]*b[i];
return dot_prod;
}
#endif
cpp代码测量每个函数所用时间
// main.cpp
#include <iostream>
#include <chrono>
#include <random>
#include "main.hpp"
int main()
{
// generate random integers
unsigned seed = std::chrono::steady_clock::now().time_since_epoch().count();
std::mt19937_64 rand_engine(seed);
std::mt19937_64 rand_engine2(seed/2);
std::uniform_int_distribution<int> random_number(0,9);
size_t LEN = 10000000;
int* a = new int[LEN];
int* b = new int[LEN];
for(size_t i=0; i<LEN; ++i)
{
a[i] = random_number(rand_engine);
b[i] = random_number(rand_engine2);
}
#ifdef SCALAR
int dot1 = 0;
#endif
#ifdef VECTOR
int dot2 = 0;
#endif
// timing
auto start = std::chrono::high_resolution_clock::now();
#ifdef SCALAR
dot1 = scalar_dot(a,b,LEN);
#endif
#ifdef VECTOR
dot2 = sse_int_dot(a,b,LEN);
#endif
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::nanoseconds>(end-start);
std::cout<<"proccess taken "<<duration.count()<<" nanoseconds\n";
#ifdef SCALAR
std::cout<<"\nScalar : Dot product = "<<dot1<<"\n";
#endif
#ifdef VECTOR
std::cout<<"\nVector : Dot product = "<<dot2<<"\n";
#endif
return 0;
}
编译:
- 内在版本:
g++ main.cpp -DVECTOR -msse4.1 -o main.o
- 普通版:
g++ main.cpp -DSCALAR -msse4.1 -o main.o
我的机器:
- 架构:x86_64
- CPU(s) : 1
- CPU 核心数:4
- 每个核心线程数:1
- 型号名称:Intel(R) Pentium(R) CPU N3700 @ 1.60GHz
- 一级缓存:96 KiB
- L1i 缓存:128 KiB
- 二级缓存:2 MiB
- 一些标志:sse、sse2、sse4_1、sse4_2
在 main.cpp
中有 10000000 个 int
数组的元素,当我编译上面的代码时在我的机器上,似乎内部函数 运行 比普通版本慢,大多数时候,内部函数需要大约 97529675 nanoseconds
有时甚至更长,而普通代码只需要大约 87568313 nanoseconds
,在这里我认为如果优化标志关闭,我的内部函数应该 运行 更快,但事实证明它确实有点慢。
所以我的问题是:
- 为什么我的内在函数 运行 比较慢? (我做错了什么吗?)
- 如何更正我的内在实现,正确的方法是什么?
- 编译器是否自动 vectorize/unroll 正常代码,即使优化标志关闭
- 根据我的机器规格,获取点积的最快方法是什么?
希望有人能帮忙,谢谢
所以根据 @Peter Cordes、@Qubit 和 @j6t 的建议,我稍微修改了代码,我现在只在循环内做乘法,然后我移动了水平在循环外添加...它设法将内部版本的性能从 97529675 nanoseconds
左右提高到 56444187 nanoseconds
左右,这比我以前的实现快得多,具有相同的编译标志和 10000000 个 int 数组元素。
这是 main.hpp
的新函数int _sse_int_dot(int* a, int* b, size_t len){
size_t vec_loop = len/4;
size_t non_vec = len%4;
size_t start_non_vec_i = len-non_vec;
int dot_product;
__m128i vdot_product = _mm_set1_epi32(0);
for(size_t i=0; i<vec_loop; ++i)
{
__m128i va = _mm_loadu_si128((__m128i*)(a+(i*4)));
__m128i vb = _mm_loadu_si128((__m128i*)(b+(i*4)));
__m128i vc = _mm_mullo_epi32(va,vb);
vdot_product = _mm_add_epi32(vdot_product,vc);
}
vdot_product = _mm_hadd_epi32(vdot_product,vdot_product);
vdot_product = _mm_hadd_epi32(vdot_product,vdot_product);
dot_product = _mm_cvtsi128_si32(vdot_product);
for(size_t i=start_non_vec_i; i<len; ++i) dot_product += a[i]*b[i];
return dot_product;
}
如果此代码还有更多需要改进的地方,请指出,现在我将把它留在这里作为答案。