[英]How to write fast c++ lazy evaluation code in Fastor or Xtensor?
我是 c++ 的新手,聽說像eigen 、 blaze 、 Fastor和Xtensor 這樣的帶有惰性求值和 simd 的庫可以快速進行矢量化操作。
我通過以下 function 測量了一些執行基本數字運算時崩潰的時間:
(更快)
using namespace Fastor;
template<typename T, size_t num>
T func2(Tensor<T,num> &u) {
Tensor<T,num> z;
for (auto k=0; k<100; ++k){
z = u * u;
z /= exp(u+u);
z *= 1.;
z *= sin(u) * cos(z);
}
return z(last);
}
(張量)
template<typename T, size_t num>
T func2(xt::xtensor_fixed<T, xt::xshape<num>> &u) {
xt::xtensor_fixed<T, xt::xshape<num>> z;
for (auto k=0; k<100; ++k){
z = u * u;
z /= xt::exp(u+u);
z *= 1.;
z *= xt::sin(u) * xt::cos(z);
}
return z(0);
}
編譯標志:
(更快)
-std=c++14 -O3 -march=native -funroll-loops -DNDEBUG -mllvm -inline-threshold=10000000 -ffp-contract=fast -mfma -I/Path/to/Fastor -DFASTOR_NO_ALIAS -DFASTOR_DISPATCH_DIV_TO_MUL_EXPR
(張量)
-std=c++14 -O3 -march=native -funroll-loops -DNDEBUG -mllvm -inline-threshold=10000000 -ffp-contract=fast -mfma -I/Path/to/xsimd/include/ -I/Path/to/xtl/include/ -I/Path/to/xtensor/include/ -I/Path/to/xtensor-blas/include/ -DXTENSOR_USE_XSIMD -lblas -llapack -DHAVE_CBLAS=1
編譯器: Apple LLVM version 10.0.0 (clang-1000.11.45.5)
處理器:2.6 GHz Intel Core i5
為了對比,我還測量了python中寫的function,經過numba.vectorize
優化
@numba.vectorize(['float64(float64)'],nopython=True)
def func(x):
for k in range(100):
z = x * x
z /= np.exp(x + x)
z *= 1.0
z *= np.sin(x) * np.cos(x)
return z
結果(以 usec 為單位)表明
---------------------------------------
num | Fastor | Xtensor | numba
---------------------------------------
100 | 286 | 201 | 13
1000 | 2789 | 1202 | 65
10000 | 29288 | 20468 | 658
100000 | 328033 | 165263 | 3166
---------------------------------------
我做錯了什么嗎? Fastor 和 Xtensor 怎么會慢 50 倍。
我如何通過使用auto
關鍵字來使用表達式模板和惰性求值?
謝謝你的幫助!
@Jérôme Richard 感謝您的幫助!
有趣的是,Fastor 和 Xtensor 無法忽略冗余的 for 循環。 不管怎樣,我對每個數值運算做了比較公平的比較。
SIMD 的因子 2 也很有意義。
(更快)
template<typename T, size_t num>
T func_exp(Tensor<T,num> &u) {
Tensor<T,num> z=u;
for (auto k=0; k<100; ++k){
z += exp( u );
}
return z(0);
}
template<typename T, size_t num>
T func_sin(Tensor<T,num> &u) {
Tensor<T,num> z=u;
for (auto k=0; k<100; ++k){
z += sin( u );
}
return z(0);
}
template<typename T, size_t num>
T func_cos(Tensor<T,num> &u) {
Tensor<T,num> z=u;
for (auto k=0; k<100; ++k){
z += cos( u );
}
return z(0);
}
template<typename T, size_t num>
T func_add(Tensor<T,num> &u) {
Tensor<T,num> z=u;
for (auto k=0; k<100; ++k){
z += u;
}
return z(0);
}
template<typename T, size_t num>
T func_mul(Tensor<T,num> &u) {
Tensor<T,num> z=u;
for (auto k=0; k<100; ++k){
z *= u;
}
return z(0);
}
template<typename T, size_t num>
T func_div(Tensor<T,num> &u) {
Tensor<T,num> z=u;
for (auto k=0; k<100; ++k){
z /= u;
}
return z(0);
}
(張量)
template<typename T, size_t nn>
T func_exp(xt::xtensor_fixed<T, xt::xshape<nn>> &u) {
xt::xtensor_fixed<T, xt::xshape<nn>> z=u;
for (auto k=0; k<100; ++k){
z += xt::exp( u );
}
return z(0);
}
template<typename T, size_t nn>
T func_sin(xt::xtensor_fixed<T, xt::xshape<nn>> &u) {
xt::xtensor_fixed<T, xt::xshape<nn>> z=u;
for (auto k=0; k<100; ++k){
z += xt::sin( u );
}
return z(0);
}
template<typename T, size_t nn>
T func_cos(xt::xtensor_fixed<T, xt::xshape<nn>> &u) {
xt::xtensor_fixed<T, xt::xshape<nn>> z=u;
for (auto k=0; k<100; ++k){
z += xt::sin( u );
}
return z(0);
}
template<typename T, size_t nn>
T func_add(xt::xtensor_fixed<T, xt::xshape<nn>> &u) {
xt::xtensor_fixed<T, xt::xshape<nn>> z=u;
for (auto k=0; k<100; ++k){
z += u;
}
return z(0);
}
template<typename T, size_t nn>
T func_mul(xt::xtensor_fixed<T, xt::xshape<nn>> &u) {
xt::xtensor_fixed<T, xt::xshape<nn>> z=u;
for (auto k=0; k<100; ++k){
z *= u;
}
return z(0);
}
template<typename T, size_t nn>
T func_div(xt::xtensor_fixed<T, xt::xshape<nn>> &u) {
xt::xtensor_fixed<T, xt::xshape<nn>> z=u;
for (auto k=0; k<100; ++k){
z /= u;
}
return z(0);
}
(麻波)
@numba.vectorize(['float64(float64)'],nopython=True)
def func_exp(u):
z = u
for k in range(100):
z += exp(u)
return z
@numba.vectorize(['float64(float64)'],nopython=True)
def func_sin(u):
z = u
for k in range(100):
z += sin(u)
return z
@numba.vectorize(['float64(float64)'],nopython=True)
def func_cos(u):
z = u
for k in range(100):
z += cos(u)
return z
@numba.vectorize(['float64(float64)'],nopython=True)
def func_add(u):
z = u
for k in range(100):
z += u
return z
@numba.vectorize(['float64(float64)'],nopython=True)
def func_mul(u):
z = u
for k in range(100):
z *= u
return z
@numba.vectorize(['float64(float64)'],nopython=True)
def func_div(u):
z = u
for k in range(100):
z *= u
return z
結果顯示
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
unit [1E-6 sec] | exp | sin | cos | add | mul | div |
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| F | X | N | F | X | N | F | X | N | F | X | N | F | X | N | F | X | N |
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
n=100 | 135/135 | 38/38 | 10 | 162/162 | 65/32 | 9 | 111/110 | 34/58 | 9 | 0.07 | 0.06 | 6.2 | 0.06 | 0.05 | 9.6 | 0.06 | 0.05 | 9.6 |
n=1000 | 850/858 | 501/399 | 110 | 1004/961| 522/491 | 94 | 917/1021| 486/450 | 92 | 20 | 43 | 57 | 22 | 40 | 91 | 279 | 275 | 91 |
n=10000 | 8113 | 4160 | 830 | 10670 | 4052 | 888 | 10094 | 3436 | 1063 | 411 | 890 | 645 | 396 | 922 | 1011 | 2493 | 2735 | 914 |
n=100000 | 84032 | 46173 | 8743 | 104808 | 48203 | 8745 | 102868 | 53948 | 8958 | 6138 | 18803 | 5672 | 6039 | 13851 | 9204 | 23404 | 33485 | 9149 |
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
像135/135
這樣的格式表示without/with
-ffast-math
的結果。
事實證明
exp
、 sin
、 cos
中的表現非常糟糕,這令人驚訝。+=
、 *=
、 /=
方面比 Numba 更差。這是 Fastor/Xtensor 的本質嗎?
我將表達式修改為
template<typename T, size_t num>
auto func_exp2(Tensor<T,num> &u) {
Tensor<T,num> z=u + 100. * exp(u);;
return z;
}
template<typename T, size_t nn>
auto func_exp2(xt::xtensor_fixed<T, xt::xshape<nn>> &u) {
xt::xtensor_fixed<T, xt::xshape<nn>> z=u + 100.*xt::exp(u);
return z;
}
@numba.vectorize(['float64(float64)'],nopython=True)
def func_exp2(u):
z = u + 100 * exp(u)
return z
它給了
-----------------------------------------------------------------
unit [1E-6 sec] | Fastor | Xtensor | Numba |
-----------------------------------------------------------------
n=100 | 0.100 | 0.066 | 1.8 |
n=1000 | 0.073 | 0.057 | 3.6 |
n=10000 | 0.086 | 0.089 | 26.7 |
n=100000 | 0.056 | 0.065 | 275.7 |
-----------------------------------------------------------------
發生了什么?
100*exp(u)
?Numpy 實現速度更快的原因是它計算的內容與其他兩個不同。
實際上,python 版本不會在表達式np.sin(x) * np.cos(x)
中讀取z
。 因此,Numba JIT 足夠聰明,只執行一次循環,證明 Fastor 和 Numba 之間存在 100 的因數。 您可以通過將range(100)
替換為range(10000000000)
並觀察相同的時間來檢查這一點。
最后,XTensor 在此基准測試中比 Fastor 更快,因為它似乎使用自己的 exp/sin/cos 快速SIMD 實現,而 Fastor 似乎使用 libm 的標量實現,證明 XTensor 和 Fastor 之間的因子為 2。
回復更新:
Fastor/Xtensor 在 exp、sin、cos 上的表現真的很差,這令人驚訝。
不,我們不能從基准中得出結論。 您正在比較的是編譯器優化代碼的能力。 在這種情況下,Numba 優於普通的 C++ 編譯器,因為它處理高級 SIMD 感知代碼,而 C++ 編譯器必須處理來自 Fastor/Xtensor 庫的大量基於模板的低級代碼。 從理論上講,我認為 C++ 編譯器應該可以應用與 Numba 相同類型的高級優化,但它更難。 此外,請注意 Numpy 傾向於創建/分配臨時 arrays 而 Fastor/Xtensor 不應該。
實際上,Numba 更快,因為u
是常數, exp(u)
、 sin(u)
和cos(u)
也是常數。 因此, Numba 會預先計算表達式(只計算一次)並仍然在循環中執行求和。 以下代碼給出了相同的時間:
@numba.vectorize(['float64(float64)'],nopython=True)
def func_exp(u):
z = u
tmp = exp(u)
for k in range(100):
z += tmp
return z
我猜 C++ 實現不會因為惰性評估而執行此優化。 在兩個 github 項目上報告此優化問題可能是個好主意。
此外,請注意u + u +... + u
並不嚴格等於100 * u
,因為浮點加法不是關聯的。 雖然-ffast-math
有助於克服此問題,但編譯器可能仍會由於優化傳遞沖突而無法執行此類優化。 例如,過多的迭代會阻止循環展開,從而阻止表達式的因式分解。
我強烈建議您執行更現實的基准測試。
Fastor/Xtensor 在 +=、*=、/= 方面比 Numba 差。
在這種情況下,Numba 可以用乘法代替常數除法(即1/u
可以預先計算)。 除此之外,請注意 Fastor 和 Numba 彼此相對接近。
為什么 Fastor/Xtensor 無法通過惰性求值將 for 循環表達為原始的 100*exp(u)?
我認為延遲評估並不意味着表達式會自動分解/優化。 相反,它意味着只應在需要時才計算結果。 然而,表達式分解可能是一個很好的特性,可以添加到未來的 Fastor/Xtensor 版本中(顯然還沒有)。
為什么 Fastor/Xtensor 隨着張量大小的增加而變得更快?
我認為它們一樣快,而不是更快(時間變化可能是噪音)。 因此,我猜這些表達式實際上並沒有計算出來。 這可能是由於延遲評估,因為z
從未被讀取過。 嘗試return z(0);
而不是return z;
(前者強制對表達式進行求值)。
我認為您誤解了惰性評估的工作原理。 C++ 是強類型語言,Python 不是。 當您執行生成表達式模板的操作時,它會生成一個新類型。
這段代碼:
using namespace Fastor;
template<typename T, size_t num>
T func2(Tensor<T,num> &u) {
Tensor<T,num> z;
for (auto k=0; k<100; ++k){
z = u * u;
z /= exp(u+u);
z *= 1.;
z *= sin(u) * cos(z);
}
return z(last);
}
不會產生您認為的效果。 z = u * u
生成表示u * u
的表達式模板,立即調用它並將其分配給z
,因為 z 的類型為 Tensor<T, num>。 為了讓表達式模板繼續循環,z 的類型必須隨着每次迭代而改變。 這在 Python 中是可能的,因為 python 是一種動態類型的語言,Fastor 和 xtensor 假設您正在嘗試在每個步驟中評估表達式,從而破壞它們執行縮減的任何機會(許多庫,如 Blaze、Eigen、Fastor, xtensor.etc 做。Fastor 的文檔甚至聲明它會嘗試使用 einsum 符號自動減少表達式,並在可能的情況下減少。它的實現具有相對復雜的成本 model 庫實際上是多么簡單)。
要在 C++ 中執行此操作,您需要展開循環並且在准備好評估之前不要分配給 z。 您可以使用std::make_index_sequence
執行此操作:
template<std::size_t ... Is, typename T>
constexpr auto unroll(std::index_sequence<Is...>, T&& expr) noexcept -> decltype(auto)
{
return ((Is, expr), ...);
}
template<std::size_t ... Is, typename T>
constexpr auto unroll_add(std::index_sequence<Is...>, T&& expr) noexcept -> decltype(auto)
{
return ((Is, expr) + ...);
}
template<typename T, size_t num>
T func2(Tensor<T,num> &u) {
Tensor<T,num> z = unroll(std::make_index_sequence<100>{},
u * u
/ exp(u+u)
* 1.
* sin(u) * cos(z)
);
return z(last);
}
template<typename T, size_t num>
T func_exp(Tensor<T,num> &u) {
Tensor<T,num> z = u + unroll_add(std::make_index_sequence<100>{},
exp( u );
);
return z(0);
}
使用表達式模板,您可以在不立即調用表達式的情況下執行這樣的多步操作:
auto&& a = u + u; // add<T&, T&>
auto&& b = a * u; // mul<add<T&, T&>, T&>
Tensor c = b;. // Tensor is evaluated here
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.