私はEigenを使っていくつかのSSEコードを書こうとしています。SSEのパフォーマンスEigen
#ifndef EIGEN_DONT_VECTORIZE // Not needed with Intel C++ Compiler XE 15.0
#define EIGEN_VECTORIZE_SSE4_2
#define EIGEN_VECTORIZE_SSE4_1
#define EIGEN_VECTORIZE_SSSE3
#define EIGEN_VECTORIZE_SSE3
#endif
#include "stdafx.h"
#include <iostream>
#include <unsupported/Eigen/AlignedVector3>
#include <Eigen/StdVector>
#include <chrono>
int _tmain(int argc, _TCHAR* argv[]) {
static const int SIZE = 4000000;
EIGEN_ALIGNED_VECTOR3 Eigen::AlignedVector3<float> A_SSE(1, 1, 1);
//EIGEN_ALIGNED_VECTOR3 Eigen::AlignedVector3<float> B_SSE(2, 2, 2);
//std::vector<Eigen::AlignedVector3<float>> C_SSE(SIZE, Eigen::AlignedVector3<float>(0,0,0));
EIGEN_ALIGNED_VECTOR3 Eigen::AlignedVector3<float> A_SSE1(1, 1, 1);
EIGEN_ALIGNED_VECTOR3 Eigen::AlignedVector3<float> A_SSE2(1, 1, 1);
EIGEN_ALIGNED_VECTOR3 Eigen::AlignedVector3<float> A_SSE3(1, 1, 1);
EIGEN_ALIGNED_VECTOR3 Eigen::AlignedVector3<float> A_SSE4(1, 1, 1);
EIGEN_ALIGNED_VECTOR3 Eigen::AlignedVector3<float> B_SSE(2, 2, 2);
EIGEN_ALIGNED_VECTOR3 Eigen::AlignedVector3<float> B_SSE_increment_unroll(16, 16, 16);
A_SSE2 += B_SSE;
A_SSE3 = A_SSE2 + B_SSE;
A_SSE4 = A_SSE3 + B_SSE;
std::vector<Eigen::AlignedVector3<float>> C_SSE(SIZE, Eigen::AlignedVector3<float>(0, 0, 0));
auto start2 = std::chrono::system_clock::now();
// no unroll
for (int iteration = 0; iteration < SIZE; ++iteration) {
A_SSE += B_SSE;
C_SSE[iteration] = A_SSE;
}
//// own unroll
//for (int iteration = 0; iteration < SIZE/8; ++iteration){
// A_SSE1 += B_SSE_increment_unroll;
// A_SSE2 += B_SSE_increment_unroll;
// A_SSE3 += B_SSE_increment_unroll;
// A_SSE4 += B_SSE_increment_unroll;
// C_SSE[iteration * 2] = A_SSE1;
// C_SSE[iteration * 2 + 1] = A_SSE2;
// C_SSE[iteration * 2 + 2] = A_SSE3;
// C_SSE[iteration * 2 + 3] = A_SSE4;
//}
auto end2 = std::chrono::system_clock::now();
auto elapsed2 = end2 - start2;
std::cout << "Eigen aligned vector " << elapsed2.count() << '\n';
Eigen::Matrix3Xf A = Eigen::Matrix3Xf::Zero(3, SIZE);
Eigen::Vector3f B(3, 3, 3);
Eigen::Vector3f C(2, 2, 2);
auto start1 = std::chrono::system_clock::now();
for (int iteration = 0; iteration < SIZE; ++iteration) {
B += C;
A.col(iteration) = B;
}
auto end1 = std::chrono::system_clock::now();
auto elapsed1 = end1 - start1;
std::cout << "Eigen matrix " << elapsed1.count() << '\n';
float *pResult = (float*)_aligned_malloc(SIZE * sizeof(float) * 4, 16); // align to 16-byte for SSE
auto start3 = std::chrono::system_clock::now();
__m128 x;
__m128 xDelta = _mm_set1_ps(2.0f); // Set the xDelta to (4,4,4,4)
__m128 *pResultSSE = (__m128*) pResult;
x = _mm_set_ps(1.0f, 1.0f, 1.0f, 1.0f); // Set the initial values of x to (4,3,2,1)
for (int iteration = 0; iteration < SIZE; ++iteration)
{
x = _mm_add_ps(x, xDelta);
pResultSSE[iteration] = x;
}
auto end3 = std::chrono::system_clock::now();
auto elapsed3 = end3 - start3;
std::cout << "Own sse " << elapsed3.count() << '\n';
}
タイミングが
- 固有値、配向ベクトルをほどき私のPC上で、奇妙に思える:20057
- 固有合わせベクトルなしアンロール:〜120320
- 固有マトリックス:コードを考えると
〜120207(アンロールなしと同じ)
- 自分のSSE:160784
私がアセンブリを調べると、アライメントされたバージョンとOwn SSEはaddps movapsを使用しますが、ループを手動でアンロールするまではパフォーマンスは向上しません。実行しても(50%どんなブーストも得られない。バージョン固有の行列はsseを使用せず、同じパフォーマンスを達成し、インラインアセンブリは16回の反復で展開を示します。手動のアンローリングはそれほど大きな影響を与えますか?私たちは手動でSSEのためにそれを行うべきですか?CPUのプロパティでオンになっているかどうかは、それが依存していますか?
編集: 合計すると。 SSE命令は、展開されていないループが展開されていない場合と同じ結果を保持することを証明できないため、パフォーマンスが向上しないため、メモリ記憶のレイテンシを隠すことができません。しかし、アセンブリコードでは、 "単一"命令は1つのレジスタしか使用せず、展開されていないループでそれをインクリメントします。 SSE中毒が垂直方向に行われた場合(整列したベクトル内の単一の浮動小数点は同じ量の加算演算を蓄積する)、コンパイラはアンローリングの平等性を証明できるはずです。 SSE操作は、デフォルトでコンパイラによって最適化されていませんか?アンロールループが実行順序を保持しているので、非連想計算が維持されるため、自動アンローリングが可能になるはずですが、どうしてそれが起こらないのでしょうか?
EDIT:固有値から としては、私がテストを実行する提案が、ベンチユニットは、Visual Studio 2017の下では動作しませんので、それは
#include <iostream>
#include <vector>
#include <unsupported/Eigen/AlignedVector3>
#include <chrono>
#include <numeric>
EIGEN_DONT_INLINE
void vector_no_unroll(std::vector<Eigen::AlignedVector3<float>>& out)
{
Eigen::AlignedVector3<float> A_SSE(1, 1, 1);
Eigen::AlignedVector3<float> B_SSE(2, 2, 2);
for (auto &x : out)
{
A_SSE += B_SSE;
x = A_SSE;
}
}
EIGEN_DONT_INLINE
void vector_unrolled(std::vector<Eigen::AlignedVector3<float>>& out)
{
Eigen::AlignedVector3<float> A_SSE1(1, 1, 1);
Eigen::AlignedVector3<float> A_SSE2(1, 1, 1);
Eigen::AlignedVector3<float> A_SSE3(1, 1, 1);
Eigen::AlignedVector3<float> A_SSE4(1, 1, 1);
Eigen::AlignedVector3<float> B_SSE(2, 2, 2);
Eigen::AlignedVector3<float> B_SSE_increment_unroll(16, 16, 16);
A_SSE2 += B_SSE;
A_SSE3 = A_SSE2 + B_SSE;
A_SSE4 = A_SSE3 + B_SSE;
for (size_t i = 0; i<out.size(); i += 4)
{
A_SSE1 += B_SSE_increment_unroll;
A_SSE2 += B_SSE_increment_unroll;
A_SSE3 += B_SSE_increment_unroll;
A_SSE4 += B_SSE_increment_unroll;
out[i + 0] = A_SSE1;
out[i + 1] = A_SSE2;
out[i + 2] = A_SSE3;
out[i + 3] = A_SSE4;
}
}
EIGEN_DONT_INLINE
void eigen_matrix(Eigen::Matrix3Xf& out)
{
Eigen::Vector3f B(1, 1, 1);
Eigen::Vector3f C(2, 2, 2);
for (int i = 0; i < out.cols(); ++i) {
B += C;
out.col(i) = B;
}
}
template<int unrolling> EIGEN_DONT_INLINE
void eigen_matrix_unrolled(Eigen::Matrix3Xf& out)
{
Eigen::Matrix<float, 3, unrolling> B = Eigen::Matrix<float, 1, unrolling>::LinSpaced(3.f, 1 + 2 * unrolling).template replicate<3, 1>();
for (int i = 0; i < out.cols(); i += unrolling) {
out.middleCols<unrolling>(i) = B;
B.array() += float(2 * unrolling);
}
}
int main() {
static const int SIZE = 4000000;
int tries = 30;
int rep = 10;
std::vector<int> Timings(tries, 0);
{
Eigen::Matrix3Xf A(3, SIZE);
#pragma loop(1)
for (int iter = 0; iter < tries; ++iter)
{
auto start1 = std::chrono::system_clock::now();
eigen_matrix(A);
Timings[iter] = (std::chrono::system_clock::now() - start1).count();
}
}
std::cout << "eigen matrix Min: " << *std::min_element(Timings.begin(), Timings.end()) << " ms\n";
std::cout << "eigen matrix Mean: " << std::accumulate(Timings.begin(), Timings.end(), 0)/tries << " ms\n";
{
Eigen::Matrix3Xf A(3, SIZE);
#pragma loop(1)
for (int iter = 0; iter < tries; ++iter)
{
auto start1 = std::chrono::system_clock::now();
eigen_matrix_unrolled<4>(A);
Timings[iter] = (std::chrono::system_clock::now() - start1).count();
}
}
std::cout << "eigen matrix unrolled 4 min: " << *std::min_element(Timings.begin(), Timings.end()) << " ms\n";
std::cout << "eigen matrix unrolled 4 Mean: " << std::accumulate(Timings.begin(), Timings.end(), 0)/tries << " ms\n";
{
Eigen::Matrix3Xf A(3, SIZE);
#pragma loop(1)
for (int iter = 0; iter < tries; ++iter)
{
auto start1 = std::chrono::system_clock::now();
eigen_matrix_unrolled<8>(A);
Timings[iter] = (std::chrono::system_clock::now() - start1).count();
}
}
std::cout << "eigen matrix unrolled 8 min: " << *std::min_element(Timings.begin(), Timings.end()) << " ms\n";
std::cout << "eigen matrix unrolled 8 Mean: " << std::accumulate(Timings.begin(), Timings.end(), 0)/tries << " ms\n";
{
std::vector<Eigen::AlignedVector3<float>> A(SIZE, Eigen::AlignedVector3<float>(0, 0, 0));
#pragma loop(1)
for (int iter = 0; iter < tries; ++iter)
{
auto start1 = std::chrono::system_clock::now();
vector_no_unroll(A);
Timings[iter] = (std::chrono::system_clock::now() - start1).count();
}
}
std::cout << "eigen vector min: " << *std::min_element(Timings.begin(), Timings.end()) << " ms\n";
std::cout << "eigen vector Mean: " << std::accumulate(Timings.begin(), Timings.end(), 0)/tries << " ms\n";
{
std::vector<Eigen::AlignedVector3<float>> A(SIZE, Eigen::AlignedVector3<float>(0, 0, 0));
#pragma loop(1)
for (int iter = 0; iter < tries; ++iter)
{
auto start1 = std::chrono::system_clock::now();
vector_unrolled(A);
Timings[iter] = (std::chrono::system_clock::now() - start1).count();
}
}
std::cout << "eigen vector unrolled min: " << *std::min_element(Timings.begin(), Timings.end()) << " ms\n";
std::cout << "eigen vector unrolled Mean: " << std::accumulate(Timings.begin(), Timings.end(), 0)/tries << " ms\n";
}
によって置き換えられ、8台の切り抜いたマシン(すべてのウィンドウ)に結果を確認したと以下の結果
固有行列分を取得:110477ミリ秒
は固有の行列が平均:131691ミリ秒
40099のMS
固有行列展開4平均:54812のMS
固有行列展開8分:40001のMS
固有行列展開8平均:51482のMS
固有ベクトル固有行列は4分を広げ分:100270のMS
固有ベクトルの意味:117316のMS
固有ベクトル展開分:59966のMS
固有ベクトル平均:65847ms
私がテストしたすべてのマシンで、最も古いものが除外されました。新しいマシンのように見えますが、小さなアンローリングは非常に有益です(結果は、1.5倍から3.5倍のスピードアップが4倍になり、アンローリングが8,16,32、または256倍であってもインクリメントしません)。
*すべての*最適化はCPUごとに*する必要があります。それはすべて根本的な質問に戻ってきます。それは遅すぎますか?答えが* yes *の場合、早すぎる最適化は避けています。これは時期尚早な最適化と思われますか? Intel SSEまたはC++ SSEですか? – Sebivor
これらのルチンを最適化するのではなく、一般的にsseを使用します。 CPUごとの最適化では、たとえばxmmのレジスタをカウントし、すべてを使用するためにアンロールしますか? – CzakCzan
ループオーバーヘッド上のフロントエンドボトルネックを回避するために、ADDPSまたはFMAのレイテンシを隠すために展開します。例えばドットプロダクトの場合:https://stackoverflow.com/questions/45113527/why-does-mulss-take-only-3-cycles-on-haswell-different-from-agners-instruction。 –