【发布时间】:2021-04-15 09:40:05
【问题描述】:
在性能敏感的代码中,我必须执行向量的仿射变换:
Y=a*X+b
其中Y 和X 是向量,a 和b 是标量。
作为提高计算速度的一种快速而简单的方法,我将并行化委托给 openMP
#pragma omp simd 指令。最近有一些空闲时间,我尝试直接使用内在函数来实现它,获得与omp 解决方案大致相同的性能。
有没有办法打败 OMP 矢量化?我可以用完 AVX2 指令。
以下代码在windows 10下测试,用VS 2019编译。
#include <iostream>
#include <armadillo>
#include <chrono>
#include <immintrin.h>
///Computes y=alpha*x+beta
inline void SumAndSetOmp(
arma::Col<double>& y /**< Result*/,
const arma::Col<double>& x /**< Input*/,
const double& alpha /**< Coefficient*/,
const double& beta /**< Offset*/)
{
auto* __restrict lhs = y.memptr();
const auto* __restrict add_rhs = x.memptr();
const auto& n = x.n_elem;
#pragma omp simd
for (arma::uword i = 0; i < n; ++i)
{
lhs[i] = add_rhs[i] * alpha + beta;
}
}
inline void SumAndSetSerial(
arma::Col<double>& y /**< Result*/,
const arma::Col<double>& x /**< Input*/,
const double& alpha /**< Coefficient*/,
const double& beta /**< Offset*/)
{
auto* lhs = y.memptr();
const auto* add_rhs = x.memptr();
const auto& n = x.n_elem;
for (arma::uword i = 0; i < n; ++i)
{
lhs[i] = add_rhs[i] * alpha + beta;
}
}
inline void SumAndSetAVX(arma::Col<double>& y /**< Result*/,
const arma::Col<double>& x /**< Input*/,
const double& alpha /**< Coefficient*/,
const double& beta /**< Offset*/)
{
//Allocate coefficients
const auto alphas = _mm256_set1_pd(alpha);
const auto betas = _mm256_set1_pd(beta);
//Extracting memory addresses
auto* __restrict pos_lhs = y.memptr();
const auto* __restrict pos_rhs = x.memptr();
//Computing sizes
const unsigned int length_array = 4;
const unsigned long long n_aligned = x.n_elem / length_array;
const unsigned int remainder = x.n_elem % length_array;
//Performing AVX instruction
for (unsigned long long i = 0; i < n_aligned; i++) {
const __m256d x_avx = _mm256_loadu_pd(pos_rhs);
const __m256d y_avx = _mm256_fmadd_pd(x_avx, alphas, betas);
_mm256_storeu_pd(pos_lhs, y_avx);
pos_rhs += length_array;
pos_lhs += length_array;
}
//Process the rest serially
for (unsigned int i = 0; i < remainder; i++) {
pos_lhs[i] = alpha * pos_rhs[i] + beta;
}
}
enum method
{
serial,
omp,
avx
};
arma::vec perform_test(const arma::vec& x, const method mtd, int trials = 100, const double alpha = 3.0, const double beta = 5.0)
{
arma::Col<double> res(x.n_elem);
const auto beg = std::chrono::steady_clock::now();
switch (mtd) {
case serial:
for (int i = 0; i < trials; i++)
SumAndSetSerial(res, x, alpha, beta);
break;
case omp:
for (int i = 0; i < trials; i++)
SumAndSetOmp(res, x, alpha, beta);
break;
case avx:
for (int i = 0; i < trials; i++)
SumAndSetAVX(res, x, alpha, beta);
break;
}
std::cout << "time:" << std::chrono::duration<double>(std::chrono::steady_clock::now() - beg).count() << "s\n";
return res;
}
//Benchmarking
double test_fun(long long int n,int trials=100, const double alpha = 3.0, const double beta = 5.0)
{
const arma::Col<double> x(n, arma::fill::randn);
const arma::Col<double> reference = alpha*x + beta;
std::cout << "Serial: ";
const auto res_serial = perform_test(x, method::serial, trials, alpha, beta);
std::cout << "OMP: ";
const auto res_omp = perform_test(x, method::omp, trials, alpha, beta);
std::cout << "AVX: ";
const auto res_avx = perform_test(x, method::avx, trials, alpha, beta);
// errors wrt the reference
const double err_serial = arma::max(arma::abs(reference - res_serial));
const double err_avx = arma::max(arma::abs(reference - res_avx));
const double err_omp = arma::max(arma::abs(reference - res_omp));
//Largest error
const double error = std::max(std::max(err_serial, err_avx), err_omp);
if (error> 1e-6)
{
throw std::runtime_error("Something is wrong!");
}
return error;
}
int main()
{
test_fun(10000000);
}
【问题讨论】:
-
犰狳不应该在本地做这样的操作吗?您是否也只有 AVX2 或 FMA?
n有多大? -
我用 Eigen 进行了测试(我知道它确实在幕后做了很多优化),它生成的 asm 与您在此处使用的内在函数版本非常相似,因此您可能非常接近最优你可以做什么。我第二个想知道
n的问题(因为如果在编译时就知道了,你可以进一步优化)。可能值得在组装时达到峰值以确保基准测试确实有效(即它没有优化所有代码),如果这对性能至关重要(肯定是-O3),那么值得使用编译标志 -
Armadillo 将这些委托给编译器,例如它甚至不使用 ?axpy 进行此类操作。 n 的值是动态的,通常在 1000-10000 范围内。指令方面,我假设一个相当新的处理器,除了 AVX512 之外的所有指令