簡體   English   中英

使用 Eigen 重復向量中的元素並在所有元素上應用一組不同的函數的最有效方法是什么?

[英]What is the most efficient way to repeat elements in a vector and apply a set of different functions across all elements using Eigen?

假設我有一個僅包含正實元素的向量,定義如下:

Eigen::VectorXd v(1.3876, 8.6983, 5.438, 3.9865, 4.5673);

我想生成一個新的向量 v2,它已經將 v 中的元素重復了 k 次。 然后我想對向量中的每個重復元素應用 k 個不同的函數。

例如,如果 v2 被 v 重復 2 次,並且我應用 floor() 和 ceil() 作為我的兩個函數,則基於上述向量的結果將是一個列向量,其值是:[1; 2; 8; 9; 5; 6; 3; 4; 4; 5]。 保留原始值的順序在這里也很重要。 這些值也是一個簡化的例子,在實踐中,我正在生成包含約 100,000 個或更多元素的向量 v 並希望使我的代碼盡可能可向量化。

由於我是從 Matlab 來到 Eigen 和 C++ 的,我首先采用的最簡單的方法是將這個 Nx1 向量轉換為 Nx2 矩陣,將地板應用於第一列,將 ceil 應用於第二列,進行轉置以獲得 2xN矩陣,然后利用矩陣的列優先性質並將 2xN 矩陣重塑為 2Nx1 向量,得到我想要的結果。 但是,對於大型向量,這將非常緩慢且效率低下。

ggael 的這個響應有效地解決了我如何通過生成索引序列和索引輸入向量來重復輸入向量中的元素。 然后我可以生成更多的索引序列,將我的函數應用於相關元素 v2 並將結果復制回它們各自的位置。 然而,這真的是最有效的方法嗎? 我沒有完全掌握寫時復制和移動語義,但我認為第二個索引表達式在某種意義上是多余的?

如果這是真的,那么我的猜測是這里的解決方案將是某種零元或一元表達式,我可以在其中定義一個接受向量的表達式、一些索引 k 和 k 表達式/函數以應用於每個元素並吐出我正在尋找的向量。 我已經閱讀了有關該主題的 Eigen 文檔,但我正在努力構建一個功能示例。 任何幫助,將不勝感激!

所以,如果我理解正確,你不想replicate (就特征方法而言)向量,你想對相同的元素應用不同的方法並為每個元素存儲結果,對嗎?

在這種情況下,每個函數按順序計算一次是最簡單的方法。 無論如何,大多數 CPU 每個時鍾周期只能執行一個(向量)內存存儲。 因此,對於簡單的一元或二元運算,您的收益有一個上限。

盡管如此,你是正確的,一個負載在技術上總是比兩個好,並且 Eigen 的一個限制是沒有很好的方法來實現這一點。

要知道,即使您手動編寫一個會生成多個輸出的循環,您也應該限制自己的輸出數量。 CPU 具有有限數量的行填充緩沖區。 IIRC Intel 建議在緊密循環中使用少於 10 個“輸出流”,否則您可能會在這些流上拖延 CPU。

另一方面是 C++ 的弱別名限制使編譯器難以對具有多個輸出的代碼進行矢量化。 所以它甚至可能是有害的。

我將如何構建此代碼

請記住,Eigen 是列優先的,就像 Matlab 一樣。 因此,每個輸出函數使用一列。 或者只是使用單獨的向量開始。

Eigen::VectorXd v = ...;
Eigen::MatrixX2d out(v.size(), 2);
out.col(0) = v.array().floor();
out.col(1) = v.array().ceil();

遵循KISS原則,這就夠了。 做一些更復雜的事情,你不會有什么收獲。 一點多線程可能會給你帶來一些東西(我猜不到因子 2),因為單個 CPU 線程不足以最大化內存帶寬,但僅此而已。

一些基准測試

這是我的基線:

int main()
{
  int rows = 100013, repetitions = 100000;
  Eigen::VectorXd v = Eigen::VectorXd::Random(rows);
  Eigen::MatrixX2d out(rows, 2);
  for(int i = 0; i < repetitions; ++i) {
    out.col(0) = v.array().floor();
    out.col(1) = v.array().ceil();
  }
}

用 gcc-11 編譯, -O3 -mavx2 -fno-math-errno我得到了 ca。 5.7 秒。

檢查匯編代碼發現良好的向量化。

普通的舊 C++ 版本:

    double* outfloor = out.data();
    double* outceil = outfloor + out.outerStride();
    const double* inarr = v.data();
    for(std::ptrdiff_t j = 0; j < rows; ++j) {
      const double vj = inarr[j];
      outfloor[j] = std::floor(vj);
      outceil[j] = std::ceil(vj);
    }

40 秒而不是 5 秒! 這個版本實際上沒有向量化,因為編譯器無法證明數組不互為別名。

接下來,讓我們使用固定大小的特征向量來讓編譯器生成向量化代碼:

    double* outfloor = out.data();
    double* outceil = outfloor + out.outerStride();
    const double* inarr = v.data();
    std::ptrdiff_t j;
    for(j = 0; j + 4 <= rows; j += 4) {
      const Eigen::Vector4d vj = Eigen::Vector4d::Map(inarr + j);
      const auto floorval = vj.array().floor();
      const auto ceilval = vj.array().ceil();
      Eigen::Vector4d::Map(outfloor + j) = floorval;
      Eigen::Vector4d::Map(outceil + j) = ceilval;;
    }
    if(j + 2 <= rows) {
      const Eigen::Vector2d vj = Eigen::Vector2d::MapAligned(inarr + j);
      const auto floorval = vj.array().floor();
      const auto ceilval = vj.array().ceil();
      Eigen::Vector2d::Map(outfloor + j) = floorval;
      Eigen::Vector2d::Map(outceil + j) = ceilval;;
      j += 2;
    }
    if(j < rows) {
      const double vj = inarr[j];
      outfloor[j] = std::floor(vj);
      outceil[j] = std::ceil(vj);      
    }

7.5 秒。 匯編器看起來不錯,完全矢量化。 我不確定為什么性能較低。 也許緩存行別名?

最后一次嘗試:我們不嘗試避免重新讀取向量,而是按塊重新讀取它,以便在我們第二次讀取它時它會在緩存中。

    const int blocksize = 64 * 1024 / sizeof(double);
    std::ptrdiff_t j;
    for(j = 0; j + blocksize <= rows; j += blocksize) {
      const auto& vj = v.segment(j, blocksize);
      auto outj = out.middleRows(j, blocksize);
      outj.col(0) = vj.array().floor();
      outj.col(1) = vj.array().ceil();
    }
    const auto& vj = v.tail(rows - j);
    auto outj = out.bottomRows(rows - j);
    outj.col(0) = vj.array().floor();
    outj.col(1) = vj.array().ceil();

5.4 秒。 所以這里有一些收獲,但還不足以證明增加的復雜性是合理的。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

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