簡體   English   中英

如何在 Fastor 或 Xtensor 中編寫快速 c++ 惰性評估代碼?

[英]How to write fast c++ lazy evaluation code in Fastor or Xtensor?

我是 c++ 的新手,聽說像eigenblazeFastorXtensor 這樣的帶有惰性求值和 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的結果。

事實證明

  1. Fastor/Xtensor 在expsincos中的表現非常糟糕,這令人驚訝。
  2. Fastor/Xtensor 在+=*=/=方面比 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     |
-----------------------------------------------------------------

發生了什么?

  1. 為什么 Fastor/Xtensor 無法通過惰性求值將 for 循環表達為原始的100*exp(u)
  2. 為什么 Fastor/Xtensor 隨着張量大小的增加而變得更快?

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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM