簡體   English   中英

稀疏矩陣密集向量乘法與編譯時已知的矩陣

[英]Sparse matrix-dense vector multiplication with matrix known at compile time

我有一個只有零和一個作為條目的稀疏矩陣(例如,形狀為 32k x 64k 和 0.01% 的非零條目,並且在非零條目的位置方面沒有可利用的模式)。 該矩陣在編譯時是已知的。 我想用包含 50% 的 1 和 0 的非稀疏向量(在編譯時未知)執行矩陣向量乘法(模 2)。 我希望這是有效的,特別是,我試圖利用矩陣在編譯時已知的事實。

以有效的格式存儲矩陣(僅保存“1”的索引)總是需要幾 MB 的 memory 並將矩陣直接嵌入到可執行文件中對我來說似乎是個好主意。 我的第一個想法是自動生成 C++ 代碼,它將所有結果向量條目分配給正確輸入條目的總和。 這看起來像這樣:

constexpr std::size_t N = 64'000;
constexpr std::size_t M = 32'000;

template<typename Bit>
void multiply(const std::array<Bit, N> &in, std::array<Bit, M> &out) {
    out[0] = (in[11200] + in[21960] + in[29430] + in[36850] + in[44352] + in[49019] + in[52014] + in[54585] + in[57077] + in[59238] + in[60360] + in[61120] + in[61867] + in[62608] + in[63352] ) % 2;
    out[1] = (in[1] + in[11201] + in[21961] + in[29431] + in[36851] + in[44353] + in[49020] + in[52015] + in[54586] + in[57078] + in[59239] + in[60361] + in[61121] + in[61868] + in[62609] + in[63353] ) % 2;
    out[2] = (in[11202] + in[21962] + in[29432] + in[36852] + in[44354] + in[49021] + in[52016] + in[54587] + in[57079] + in[59240] + in[60362] + in[61122] + in[61869] + in[62610] + in[63354] ) % 2;
    out[3] = (in[56836] + in[11203] + in[21963] + in[29433] + in[36853] + in[44355] + in[49022] + in[52017] + in[54588] + in[57080] + in[59241] + in[60110] + in[61123] + in[61870] + in[62588] + in[63355] ) % 2;
    // LOTS more of this...
    out[31999] = (in[10208] + in[21245] + in[29208] + in[36797] + in[40359] + in[48193] + in[52009] + in[54545] + in[56941] + in[59093] + in[60255] + in[61025] + in[61779] + in[62309] + in[62616] + in[63858] ) % 2;
}

這確實有效(編譯需要很長時間)。 然而,它實際上似乎非常慢(比 Julia 中相同的稀疏向量矩陣乘法慢 10 倍多),而且會大大增加可執行文件的大小,遠遠超出我的預期。 我用std::arraystd::vector嘗試了這個,並且各個條目(表示為Bit )是boolstd::uint8_tint ,沒有值得一提的進展。 我還嘗試用 XOR 替換模和加法。 總之,這是一個可怕的想法。 我不確定為什么 - 純粹的代碼大小會減慢它的速度嗎? 這種代碼是否排除了編譯器優化?

我還沒有嘗試過任何替代方案。 我的下一個想法是將索引存儲為編譯時常量 arrays (仍然給我巨大的.cpp文件)並循環它們。 最初,我預計這樣做會導致編譯器優化生成與我自動生成的 C++ 代碼相同的二進制文件。 你認為這值得嘗試嗎(我想我還是會在星期一嘗試)?

另一個想法是嘗試將輸入(也許還有 output?)向量存儲為打包位並執行這樣的計算。 我希望一個人無法繞過很多位移或與操作,這最終會變得更慢更糟。

您對如何做到這一點有任何其他想法嗎?

我不確定為什么 - 純粹的代碼大小會減慢它的速度嗎?

問題是可執行文件很大,操作系統會從您的存儲設備中獲取大量頁面。 這個過程非常緩慢。 處理器通常會停止等待數據加載。 即使代碼已經加載到 RAM 中(操作系統緩存),它也會效率低下,因為 RAM 的速度(延遲 + 吞吐量)非常糟糕。 這里的主要問題是所有指令只執行一次 如果您重用 function,則需要從緩存中重新加載代碼,如果它太大而無法放入緩存中,它將從慢速 RAM 中加載。 因此,與實際執行相比,加載代碼的開銷非常高。 為了克服這個問題,您需要使用一個非常小的代碼,其中循環迭代相當少量的數據

這種代碼是否排除了編譯器優化?

這取決於編譯器,但大多數主流編譯器(例如 GCC 或 Clang)將以相同的方式優化代碼(因此編譯時間很慢)。

你認為這值得嘗試嗎(我想我還是會在星期一嘗試)?

是的,這個解決方案顯然更好,特別是如果索引以緊湊的方式存儲。 在您的情況下,您可以使用 uint16_t 類型存儲它們。 所有索引都可以放在一個大緩沖區中。 每行索引的開始/結束 position 可以在引用第一個緩沖區(或使用指針)的另一個緩沖區中指定。 此緩沖區可以在應用程序開頭的 memory 中從專用文件加載一次,以減小生成程序的大小(並避免在關鍵循環中從存儲設備中提取)。 具有非零值的概率為 0.01%,生成的數據結構將占用少於 500 KiB 的 RAM。 在一般的主流桌面處理器上,它可以放入 L3 緩存(相當快),我認為假設multiply代碼經過仔細優化,您的計算時間不應超過 1 毫秒。

另一個想法是嘗試將輸入(也許還有 output?)向量存儲為打包位並執行這樣的計算。

僅當您的矩陣不太稀疏時,位打包才是好的。 對於一個填充了 50% 的非零值的矩陣,位打包方法非常棒。 對於 0.01% 的非零值,位打包方法顯然很糟糕,因為它占用了太多空間。

我希望一個人無法繞過很多位移或與操作,這最終會變得更慢更糟。

如前所述,從存儲設備或 RAM 加載數據非常慢。 在任何現代主流處理器上進行一些位移都非常快(並且比加載數據快得多)。

以下是計算機可以執行的各種操作的大致時間:

在此處輸入圖像描述

我實現了第二種方法( constexpr arrays 以壓縮列存儲格式存儲矩陣),它好多了。 (對於包含 35'000 個的 64'000 x 22'000 二進制矩陣)使用-O3編譯需要 <1 分鍾,並在我的筆記本電腦上在 <300 微秒內執行一次乘法(對於相同的計算,Julia 大約需要 350 微秒)。 總可執行文件大小約為 1 MB。

也許一個人仍然可以做得更好。 如果有人有想法,請告訴我!

下面是一個代碼示例(顯示一個 5x10 矩陣),說明了我所做的。

#include <iostream>
#include <array>

// Compressed sparse column storage for binary matrix
constexpr std::size_t M = 5;
constexpr std::size_t N = 10;
constexpr std::size_t num_nz = 5;
constexpr std::array<std::uint16_t, N + 1> colptr = {
0x0,0x1,0x2,0x3,0x4,0x5,0x5,0x5,0x5,0x5,0x5
};
constexpr std::array<std::uint16_t, num_nz> row_idx = {
0x0,0x1,0x2,0x3,0x4
};

template<typename Bit>
constexpr void encode(const std::array<Bit, N>& in, std::array<Bit, M>& out) {

    for (std::size_t col = 0; col < N; col++) {
        for (std::size_t j = colptr[col]; j < colptr[col + 1]; j++) {
            out[row_idx[j]] = (static_cast<bool>(out[row_idx[j]]) != static_cast<bool>(in[col]));
        }
    }
}

int main() {
    using Bit = bool;
    std::array<Bit, N> input{1, 0, 1, 0, 1, 1, 0, 1, 0, 1};
    std::array<Bit, M> output{};
    
    for (auto i : input) std::cout << i;
    std::cout << std::endl;

    encode(input, output);

    for (auto i : output) std::cout << i;
}

暫無
暫無

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

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