Doubleのstd :: vectorがあるとします。つまり、
std::vector<double> MyVec(N);
N
は非常に大きいため、パフォーマンスが重要です。 MyVec
が自明でないベクトルであると仮定します(つまり、ゼロのベクトルではありませんが、何らかのルーチンによって変更されています)。さて、ベクターの否定バージョンが必要です:-MyVec
が必要です。
これまでのところ、私はそれを実装しています
std::transform(MyVec.cbegin(),MyVec.cend(),MyVec.begin(),std::negate<double>());
しかし、本当に、これが賢明なことなのか、それとも私の側から見ただけの素朴なものなのかはわかりません。
正しくやっていますか?またはこの場合、std :: transformは超低速のルーチンですか?
PS:私は常にBLASおよびLAPACKライブラリを使用していますが、この特定のニーズに合うものは見つかりませんでした。ただし、BLAS/LAPACKにstd :: transformよりも高速なこのような関数が存在する場合は、喜んでお知らせします。
#include <vector>
#include <algorithm>
#include <functional>
void check()
{
std::vector<double> MyVec(255);
std::transform(MyVec.cbegin(),MyVec.cend(),MyVec.begin(),std::negate<double>());
}
このコードは https://godbolt.org/ にcopileオプション-O3を使用してNice Assemblyを生成します
.L3:
[...]
cmp r8, 254
je .L4
movsd xmm0, QWORD PTR [rdi+2032]
xorpd xmm0, XMMWORD PTR .LC0[rip]
movsd QWORD PTR [rdi+2032], xmm0
.L4:
より速く想像することは困難です。あなたのコードはすでに完璧です。コンパイラを出し抜いて、ほとんど毎回動作するクリーンなC++コードを使用しないでください。
幸いなことにstd::vector
のデータは連続しているため、ベクトル組み込み関数を使用して-1を乗算できます(アライメントされていないロード/ストアとオーバーフローの特別な処理を使用)。または、インテルのIPPライブラリのippsMulC_64f
/ippsMulC_64f_I
を使用します(より速く何かを書くのに苦労します)。プラットフォームで利用可能な最大のベクトルレジスタを使用します。 https://software.intel .com/en-us/ipp-dev-reference-mulc
更新:コメントの混乱を解消するために、Intel IPPのフルバージョンは無料で(サポートに料金を支払うことができます)、Linux、Windows、およびmacOSに搭載されています。
他の人が述べたように、それはあなたのユースケースに完全に依存します。おそらく最も簡単な方法は次のようなものでしょう:
struct MyNegatingVect {
MyVect data;
bool negated = false;
void negate() { negated = !negated; }
// ... setter and getter need indirection ...
// ..for example
MyVect::data_type at(size_t index) { return negated ? - data.at(index) : data.at(index);
};
単一のアクセスごとにこの追加の間接指定が否定を単一のbool
の設定に変換する価値があるかどうかは、既に述べたように、ユースケースに依存します(実際、これが測定可能なメリットをもたらすユースケースがあるとは思いません)。
最初に、算術型ベクトル用の汎用negate
関数を例として示します。
#include <type_traits>
#include <vector>
...
template <typename arithmetic_type> std::vector<arithmetic_type> &
negate (std::vector<arithmetic_type> & v)
{
static_assert(std::is_arithmetic<arithmetic_type>::value,
"negate: not an arithmetic type vector");
for (auto & vi : v) vi = - vi;
// note: anticipate that a range-based for may be more amenable
// to loop-unrolling, vectorization, etc., due to fewer compiler
// template transforms, and contiguous memory / stride.
// in theory, std::transform may generate the same code, despite
// being less concise. very large vectors *may* possibly benefit
// from C++17's 'std::execution::par_unseq' policy?
return v;
}
正規単項operator -
関数では、次の形式で一時的なものを作成する必要があります。
std::vector<double> operator - (const std::vector<double> & v)
{
auto ret (v); return negate(ret);
}
または一般的に:
template <typename arithmetic_type> std::vector<arithmetic_type>
operator - (const std::vector<arithmetic_type> & v)
{
auto ret (v); return negate(ret);
}
not演算子を次のように実装するように誘惑されます:
template <typename arithmetic_type> std::vector<arithmetic_type> &
operator - (std::vector<arithmetic_type> & v)
{
return negate(v);
}
(- v)
は、要素を無効にし、一時的なものを必要とせずに変更されたベクトルを返します。効果的に設定することにより、数学的な規則を破ります:v = - v;
それが目標であれば、negate
関数を使用します。期待されるオペレーター評価を壊さないでください!
avx512を有効にしたclangは、このループを生成し、反復ごとに印象的な64倍を無効にします(前後の長さの処理の間)。
vpbroadcastq LCPI0_0(%rip), %zmm0
.p2align 4, 0x90
LBB0_21:
vpxorq -448(%rsi), %zmm0, %zmm1
vpxorq -384(%rsi), %zmm0, %zmm2
vpxorq -320(%rsi), %zmm0, %zmm3
vpxorq -256(%rsi), %zmm0, %zmm4
vmovdqu64 %zmm1, -448(%rsi)
vmovdqu64 %zmm2, -384(%rsi)
vmovdqu64 %zmm3, -320(%rsi)
vmovdqu64 %zmm4, -256(%rsi)
vpxorq -192(%rsi), %zmm0, %zmm1
vpxorq -128(%rsi), %zmm0, %zmm2
vpxorq -64(%rsi), %zmm0, %zmm3
vpxorq (%rsi), %zmm0, %zmm4
vmovdqu64 %zmm1, -192(%rsi)
vmovdqu64 %zmm2, -128(%rsi)
vmovdqu64 %zmm3, -64(%rsi)
vmovdqu64 %zmm4, (%rsi)
addq $512, %rsi ## imm = 0x200
addq $-64, %rdx
jne LBB0_21
gcc-7.2.0は同様のループを生成しますが、インデックス付きアドレス指定を要求しているようです。
For_eachを使用する
std::for_each(MyVec.begin(), MyVec.end(), [](double& val) { val = -val });
またはC++ 17パラレル
std::for_each(std::execution::par_unseq, MyVec.begin(), MyVec.end(), [](double& val) { val = -val });