簡體   English   中英

使用數組時的抽象與性能

[英]Abstraction vs. performance when working with arrays

這是一個關於在處理數組時我要在更好的性能和更清晰的代碼(更好的抽象)之間進行選擇的問題。 我試圖將其提煉成一個玩具的例子。

C ++特別擅長允許抽象而不損害性能。 問題是,在與以下示例類似的示例中是否可能這樣做。

考慮一個使用連續的行主存儲的瑣碎的任意大小矩陣類:

#include <cmath>
#include <cassert>

class Matrix {
    int nrow, ncol;
    double *data;
public:
    Matrix(int nrow, int ncol) : nrow(nrow), ncol(ncol), data(new double[nrow*ncol]) { }
    ~Matrix() { delete [] data; }

    int rows() const { return nrow; }
    int cols() const { return ncol; }

    double & operator [] (int i) { return data[i]; }

    double & operator () (int i, int j) { return data[i*ncol + j]; }
};

它具有一個2D索引operator () ,使其易於使用。 它還具有用於連續訪問的operator [] ,但是抽象性更好的矩陣可能沒有此值。

讓我們實現一個函數,該函數采用n×2矩陣,本質上是2D向量列表,並就地標准化每個向量。

明確的方法:

inline double veclen(double x, double y) {
    return std::sqrt(x*x + y*y);
}

void normalize(Matrix &mat) {
    assert(mat.cols() == 2); // some kind of check for correct input
    for (int i=0; i < mat.rows(); ++i) {
        double norm = veclen(mat(i,0), mat(i,1));
        mat(i,0) /= norm;
        mat(i,1) /= norm;
    }
}

快速但不太清楚的方式:

void normalize2(Matrix &mat) {
    assert(mat.cols() == 2);
    for (int i=0; i < mat.rows(); ++i) {
        double norm = veclen(mat[2*i], mat[2*i+1]);
        mat[2*i] /= norm;
        mat[2*i+1] /= norm;
    }
}

第二個版本( normalize2 )可能會更快,因為它的編寫方式很顯然,循環的第二個迭代將不訪問在第一次迭代中計算出的數據。 因此,它可以潛在地更好地利用SIMD指令。 看着天幕,這似乎是發生的事情 (除非我誤讀了程序集)。

在第一個版本( normalize )中,編譯器無法知道輸入矩陣不是nby-1,這將導致重疊的數組訪問。

問題:是否可以某種方式告訴編譯器輸入矩陣在normalize()實際上是n-by-2,以使其可以優化到與normalize2()相同的水平?


解決意見:

  • John Zwinck:我去做了基准測試。 normalize2()的速度要快得多(2.4秒與1.3秒),但是僅當我刪除assert宏或定義NDEBUG 時才如此 這是-DNDEBUG的相當違反直覺的效果,不是嗎? 它降低了性能而不是提高了性能。

  • 馬克斯:證據既是我鏈接的指標,也是上述基准。 對於這兩個函數無法內聯的情況,我也很感興趣(例如,因為它們在單獨的翻譯單元中)。

  • Jarod42和bolov:這是我一直在尋找的答案。 由第一點提到的基准確認。 盡管如此,了解一個人實現自己的assert (這正是我在我的應用程序中所做的事情)的assert下,了解這一點仍然很重要。

我相信模板可以使您同時獲得性能和可讀性。

通過確定矩陣的大小(就像流行的數學庫一樣),您可以讓編譯器在編譯時知道很多信息。

我修改了您的小課:

template<int R, int C>
class Matrix {
    double data[R * C] = {0.0};
public:
    Matrix() = default;

    int rows() const { return R; }
    int cols() const { return C; }
    int size() const { return R*C; }

    double & operator [] (int i) { return data[i]; }

    double & operator () (int row, int col) { return data[row*C + col]; }
};

inline double veclen(double x, double y) {
    return std::sqrt(x*x + y*y);
}

template<int R>
void normalize(Matrix<R, 2> &mat) {
    for (int i = 0; i < R; ++i) {
        double norm = veclen(mat(i, 0), mat(i, 1));
        mat(i, 0) /= norm;
        mat(i, 1) /= norm;
    }
}

template<int R>
void normalize2(Matrix<R, 2> &mat) {
    for (int i = 0; i < R; ++i) {
        double norm = veclen(mat[2 * i], mat[2 * i + 1]);
        mat[2 * i] /= norm;
        mat[2 * i + 1] /= norm;
    }
}

我還更喜歡將數據作為普通成員(=不帶指針)放置,因此您可以在矩陣構造期間選擇內存所在的位置(堆棧或堆)。

額外的好處是,您現在可以在編譯時確定正常化函數僅接受n-by-2矩陣。

我沒有在編譯器資源管理器中測試我的代碼,因為老實說我無法破譯asm。 所以,是的,我不確定我的版本會更好;)

最后一句話:不要滾動自己的矩陣,而要使用glm或本征之類的庫。

最后一句話²:如果您不知道要選擇什么,則更喜歡可讀性。

@bolov和@ Jared42在評論中基本上給出了我可以接受的答案。 由於他們沒有發布,因此我會自己發布。

為了讓編譯器知道矩陣的大小為n×2,將代碼添加到函數的開頭就足夠了,當矩陣大小不正確時,該代碼的其余部分將無法訪問。

例如,添加

if (mat.cols() != 2)
    throw std::runtime_error("Input array is not of expected shape.");

normalize()開始的位置,它的運行速度與normalize2()完全一樣normalize2()在我使用clang 5.0的基准測試中為1.3秒而不是2.4秒)。

我們還可以添加assert(mat.cols() == 2) ,但這會產生反直覺的效果, -DNDEBUG在編譯過程中定義-DNDEBUG會使函數變慢(因為它刪除了斷言)。

暫無
暫無

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

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