簡體   English   中英

多進程 MPI 與多線程 std::thread 性能

[英]multi-process MPI vs. multithreaded std::thread performance

我編寫了一個簡單的測試程序來比較使用 MPI 在多個進程上並行化的性能,或者使用std::thread在多個線程上進行並行化的性能。 並行化的工作只是寫入一個大數組。 我所看到的是多進程 MPI 在相當大的程度上勝過多線程。

測試代碼為:

#ifdef USE_MPI
#include <mpi.h>
#else
#include <thread>
#endif
#include <iostream>
#include <vector>

void dowork(int i){
    int n = 1000000000;
    std::vector<int> foo(n, -1);
}

int main(int argc, char *argv[]){
    int npar = 1;
#ifdef USE_MPI
    MPI_Init(&argc, &argv);
    MPI_Comm_size(MPI_COMM_WORLD, &npar);
#else
    npar = 8;
    if(argc > 1){
        npar = atoi(argv[1]);
    }
#endif
    std::cout << "npar = " << npar << std::endl;

    int i;

#ifdef USE_MPI
    MPI_Comm_rank(MPI_COMM_WORLD, &i);
    dowork(i);
    MPI_Finalize();
#else
    std::vector<std::thread> threads;
    for(i = 0; i < npar; ++i){
        threads.emplace_back([i](){
            dowork(i);
        });
    }
    for(i = 0; i < npar; ++i){
        threads[i].join();
    }
#endif
    return 0;
}

Makefile 是:

partest_mpi:
    mpic++ -O2 -DUSE_MPI  partest.cpp -o partest_mpi -lmpi
partest_threads:
    c++ -O2 partest.cpp -o partest_threads -lpthread

並且執行的結果是:

$ time ./partest_threads 8
npar = 8

real    0m2.524s
user    0m4.691s
sys 0m9.330s

$ time mpirun -np 8 ./partest_mpi
npar = 8
npar = 8
npar = 8
npar = 8
npar = 8
npar = 8
npar = 8npar = 8


real    0m1.811s
user    0m4.817s
sys 0m9.011s

所以問題是,為什么會發生這種情況,我可以對線程代碼做些什么來使其性能更好? 我猜這與 memory 帶寬和緩存利用率有關。 我在 Intel i9-9820X 10 核 CPU 上運行它。

TL;DR:確保你有足夠的 RAM 和基准指標是准確的。 話雖如此,我無法在我的機器上重現這種差異(即我得到相同的性能結果)。

在大多數平台上,您的代碼分配 30 GB(因為sizeof(int)=4並且每個進程/線程執行向量的分配,並且項目由向量初始化)。 因此,您應該首先確保至少有足夠的 RAM 來執行此操作。 否則,由於內存交換,數據可能會寫入(慢得多)存儲設備(例如 SSD/HDD)。 在這種極端情況下,基准並不是真正有用的(特別是因為結果可能不穩定)。

假設您有足夠的 RAM,您的應用程序主要受page-faults約束。 事實上,在大多數現代主流平台上,操作系統 (OS) 會非常快速地分配虛擬內存,但不會直接將其映射到物理內存。 這個映射過程通常在第一次讀取/寫入頁面(即頁面錯誤)並且已知為慢時完成 此外,出於安全原因(例如,不泄露其他進程的憑據),大多數操作系統會在第一次寫入每個頁面時將其歸零,從而使頁面錯誤更慢。 在某些系統上,它可能無法很好地擴展(盡管在具有 Windows/Linux/Mac 的典型台式機上應該沒問題)。 這部分時間報告為系統時間

其余時間主要用於填充 RAM 中的向量所需的時間。 這部分幾乎無法在許多平台上擴展:通常 2-3 個內核顯然足以使台式機的 RAM 帶寬飽和。

話雖如此,在我的機器上,我無法在分配的內存減少 10 倍的情況下重現相同的結果(因為我沒有 30 GB 的 RAM)。 同樣適用於減少 4 倍的內存。 實際上,我的帶有 i7-9600KF 的 Linux 機器上的 MPI 版本要慢得多。 請注意,結果相對穩定且可重現(無論排序和運行次數如何):

time ./partest_threads 6 > /dev/null
real    0m0,188s
user    0m0,204s
sys 0m0,859s

time mpirun -np 6 ./partest_mpi > /dev/null
real    0m0,567s
user    0m0,365s
sys 0m0,991s

MPI 版本的糟糕結果來自我機器上MPI 運行時緩慢初始化,因為一個不執行任何操作的程序大約需要 350 毫秒才能初始化。 這實際上表明行為是平台相關的。 至少,它表明不應該用time來衡量兩個應用程序的性能。 人們應該改用單調的 C++ 時鍾

一旦代碼被修復為使用准確的計時方法(使用 C++ 時鍾和 MPI 屏障),我會在兩個實現之間獲得非常接近的性能結果(10 次運行,排序計時):

pthreads:
Time: 0.182812 s
Time: 0.186766 s
Time: 0.187641 s
Time: 0.18785 s
Time: 0.18797 s
Time: 0.188256 s
Time: 0.18879 s
Time: 0.189314 s
Time: 0.189438 s
Time: 0.189501 s
Median time: 0.188 s

mpirun:
Time: 0.185664 s
Time: 0.185946 s
Time: 0.187384 s
Time: 0.187696 s
Time: 0.188034 s
Time: 0.188178 s
Time: 0.188201 s
Time: 0.188396 s
Time: 0.188607 s
Time: 0.189208 s
Median time: 0.188 s

要對 Linux 進行更深入的分析,您可以使用perf工具。 內核端分析顯示,大部分時間(60-80%)都花在了內核函數clear_page_erms ,它在頁面錯誤(如前所述)期間將頁面歸零,然后是填充向量值的__memset_avx2_erms 其他函數只占用總運行時間的一小部分。 這是 pthread 的示例:

  64,24%  partest_threads  [kernel.kallsyms]              [k] clear_page_erms
  18,80%  partest_threads  libc-2.31.so                   [.] __memset_avx2_erms
   2,07%  partest_threads  [kernel.kallsyms]              [k] prep_compound_page
   0,86%  :8444            [kernel.kallsyms]              [k] clear_page_erms
   0,82%  :8443            [kernel.kallsyms]              [k] clear_page_erms
   0,74%  :8445            [kernel.kallsyms]              [k] clear_page_erms
   0,73%  :8446            [kernel.kallsyms]              [k] clear_page_erms
   0,70%  :8442            [kernel.kallsyms]              [k] clear_page_erms
   0,69%  :8441            [kernel.kallsyms]              [k] clear_page_erms
   0,68%  partest_threads  [kernel.kallsyms]              [k] kernel_init_free_pages
   0,66%  partest_threads  [kernel.kallsyms]              [k] clear_subpage
   0,62%  partest_threads  [kernel.kallsyms]              [k] get_page_from_freelist
   0,41%  partest_threads  [kernel.kallsyms]              [k] __free_pages_ok
   0,37%  partest_threads  [kernel.kallsyms]              [k] _cond_resched
[...]  

如果兩個實現中的一個存在任何內部安全性能開銷, perf應該能夠報告它。 如果您在 Windows 上運行,則可以使用其他分析工具,例如 VTune。

暫無
暫無

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

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