主頁 > 軟體設計 > 為什么在單獨的回圈中按元素添加比在組合回圈中快得多?

為什么在單獨的回圈中按元素添加比在組合回圈中快得多?

2022-11-17 03:14:59 軟體設計

假設a1, b1, c1, 和d1指向堆記憶體,我的數值代碼有以下核心回圈。

const int n = 100000;

for (int j = 0; j < n; j  ) {
    a1[j]  = b1[j];
    c1[j]  = d1[j];
}

該回圈通過另一個外部for回圈執行了 10,000 次。為了加快速度,我將代碼更改為:

for (int j = 0; j < n; j  ) {
    a1[j]  = b1[j];
}

for (int j = 0; j < n; j  ) {
    c1[j]  = d1[j];
}

在Microsoft Visual C 10.0上編譯,并在Intel Core 2 Duo (x64)上為 32 位啟用了完全優化和SSE2,第一個示例耗時 5.5 秒,而雙回圈示例僅耗時 1.9 秒。

第一個回圈的反匯編基本上是這樣的(這個塊在整個程式中重復了大約五次):

movsd       xmm0,mmword ptr [edx 18h]
addsd       xmm0,mmword ptr [ecx 20h]
movsd       mmword ptr [ecx 20h],xmm0
movsd       xmm0,mmword ptr [esi 10h]
addsd       xmm0,mmword ptr [eax 30h]
movsd       mmword ptr [eax 30h],xmm0
movsd       xmm0,mmword ptr [edx 20h]
addsd       xmm0,mmword ptr [ecx 28h]
movsd       mmword ptr [ecx 28h],xmm0
movsd       xmm0,mmword ptr [esi 18h]
addsd       xmm0,mmword ptr [eax 38h]

雙回圈示例的每個回圈都會生成此代碼(以下塊重復大約三次):

addsd       xmm0,mmword ptr [eax 28h]
movsd       mmword ptr [eax 28h],xmm0
movsd       xmm0,mmword ptr [ecx 20h]
addsd       xmm0,mmword ptr [eax 30h]
movsd       mmword ptr [eax 30h],xmm0
movsd       xmm0,mmword ptr [ecx 28h]
addsd       xmm0,mmword ptr [eax 38h]
movsd       mmword ptr [eax 38h],xmm0
movsd       xmm0,mmword ptr [ecx 30h]
addsd       xmm0,mmword ptr [eax 40h]
movsd       mmword ptr [eax 40h],xmm0

結果證明這個問題無關緊要,因為行為嚴重取決于陣列 (n) 的大小和 CPU 快取。因此,如果有進一步的興趣,我改寫這個問題:

  • 您能否對導致不同快取行為的細節提供一些可靠的見解,如下圖中的五個區域所示?

  • 通過為這些 CPU 提供類似的圖表來指出 CPU/快取架構之間的差異可能也很有趣。

這是完整的代碼。它使用TBB Tick_Count進行更高解析度的計時,可以通過不定義TBB_TIMING宏來禁用它:

#include <iostream>
#include <iomanip>
#include <cmath>
#include <string>

//#define TBB_TIMING

#ifdef TBB_TIMING   
#include <tbb/tick_count.h>
using tbb::tick_count;
#else
#include <time.h>
#endif

using namespace std;

//#define preallocate_memory new_cont

enum { new_cont, new_sep };

double *a1, *b1, *c1, *d1;


void allo(int cont, int n)
{
    switch(cont) {
      case new_cont:
        a1 = new double[n*4];
        b1 = a1   n;
        c1 = b1   n;
        d1 = c1   n;
        break;
      case new_sep:
        a1 = new double[n];
        b1 = new double[n];
        c1 = new double[n];
        d1 = new double[n];
        break;
    }

    for (int i = 0; i < n; i  ) {
        a1[i] = 1.0;
        d1[i] = 1.0;
        c1[i] = 1.0;
        b1[i] = 1.0;
    }
}

void ff(int cont)
{
    switch(cont){
      case new_sep:
        delete[] b1;
        delete[] c1;
        delete[] d1;
      case new_cont:
        delete[] a1;
    }
}

double plain(int n, int m, int cont, int loops)
{
#ifndef preallocate_memory
    allo(cont,n);
#endif

#ifdef TBB_TIMING   
    tick_count t0 = tick_count::now();
#else
    clock_t start = clock();
#endif
        
    if (loops == 1) {
        for (int i = 0; i < m; i  ) {
            for (int j = 0; j < n; j  ){
                a1[j]  = b1[j];
                c1[j]  = d1[j];
            }
        }
    } else {
        for (int i = 0; i < m; i  ) {
            for (int j = 0; j < n; j  ) {
                a1[j]  = b1[j];
            }
            for (int j = 0; j < n; j  ) {
                c1[j]  = d1[j];
            }
        }
    }
    double ret;

#ifdef TBB_TIMING   
    tick_count t1 = tick_count::now();
    ret = 2.0*double(n)*double(m)/(t1-t0).seconds();
#else
    clock_t end = clock();
    ret = 2.0*double(n)*double(m)/(double)(end - start) *double(CLOCKS_PER_SEC);
#endif
    
#ifndef preallocate_memory
    ff(cont);
#endif

    return ret;
}


void main()
{   
    freopen("C:\\test.csv", "w", stdout);

    char *s = " ";

    string na[2] ={"new_cont", "new_sep"};

    cout << "n";

    for (int j = 0; j < 2; j  )
        for (int i = 1; i <= 2; i  )
#ifdef preallocate_memory
            cout << s << i << "_loops_" << na[preallocate_memory];
#else
            cout << s << i << "_loops_" << na[j];
#endif
            
    cout << endl;

    long long nmax = 1000000;

#ifdef preallocate_memory
    allo(preallocate_memory, nmax);
#endif
    
    for (long long n = 1L; n < nmax; n = max(n 1, long long(n*1.2)))
    {
        const long long m = 10000000/n;
        cout << n;

        for (int j = 0; j < 2; j  )
            for (int i = 1; i <= 2; i  )
                cout << s << plain(n, m, j, i);
        cout << endl;
    }
}

它針對不同的值顯示 FLOP/s n

為什么在單獨的回圈中按元素添加比在組合回圈中快得多?

uj5u.com熱心網友回復:

進一步分析,我認為這是(至少部分)由四分球的資料對齊造成的。這將導致某種程度的快取組/方式沖突。

如果我猜對了您分配陣列的方式,它們很可能與頁面行對齊

這意味著您在每個回圈中的所有訪問都將落在相同的快取方式上。然而,Intel 處理器已經有一段時間具有 8 路 L1 快取關聯性。但實際上,性能并不完全一致。訪問 4 路仍然比說 2 路慢。

編輯:實際上看起來您正在分別分配所有陣列。 通常當請求如此大的分配時,分配器將從作業系統請求新頁面。因此,大分配很有可能出現在與頁面邊界相同的偏移處。

這是測驗代碼:

int main(){
    const int n = 100000;

#ifdef ALLOCATE_SEPERATE
    double *a1 = (double*)malloc(n * sizeof(double));
    double *b1 = (double*)malloc(n * sizeof(double));
    double *c1 = (double*)malloc(n * sizeof(double));
    double *d1 = (double*)malloc(n * sizeof(double));
#else
    double *a1 = (double*)malloc(n * sizeof(double) * 4);
    double *b1 = a1   n;
    double *c1 = b1   n;
    double *d1 = c1   n;
#endif

    //  Zero the data to prevent any chance of denormals.
    memset(a1,0,n * sizeof(double));
    memset(b1,0,n * sizeof(double));
    memset(c1,0,n * sizeof(double));
    memset(d1,0,n * sizeof(double));

    //  Print the addresses
    cout << a1 << endl;
    cout << b1 << endl;
    cout << c1 << endl;
    cout << d1 << endl;

    clock_t start = clock();

    int c = 0;
    while (c   < 10000){

#if ONE_LOOP
        for(int j=0;j<n;j  ){
            a1[j]  = b1[j];
            c1[j]  = d1[j];
        }
#else
        for(int j=0;j<n;j  ){
            a1[j]  = b1[j];
        }
        for(int j=0;j<n;j  ){
            c1[j]  = d1[j];
        }
#endif

    }

    clock_t end = clock();
    cout << "seconds = " << (double)(end - start) / CLOCKS_PER_SEC << endl;

    system("pause");
    return 0;
}

基準測驗結果:

編輯:在實際的Core 2 架構機器上的結果:

2 x Intel Xeon X5482 Harpertown @ 3.2 GHz:

#define ALLOCATE_SEPERATE
#define ONE_LOOP
00600020
006D0020
007A0020
00870020
seconds = 6.206

#define ALLOCATE_SEPERATE
//#define ONE_LOOP
005E0020
006B0020
00780020
00850020
seconds = 2.116

//#define ALLOCATE_SEPERATE
#define ONE_LOOP
00570020
00633520
006F6A20
007B9F20
seconds = 1.894

//#define ALLOCATE_SEPERATE
//#define ONE_LOOP
008C0020
00983520
00A46A20
00B09F20
seconds = 1.993

觀察:

  • 一個回圈6.206 秒,兩個回圈2.116 秒這準確地再現了 OP 的結果。

  • 在前兩個測驗中,陣列是分開分配的。您會注意到它們都相對于頁面具有相同的對齊方式。

  • 在后兩個測驗中,陣列被打包在一起以打破這種對齊方式。在這里您會注意到兩個回圈都更快。此外,第二個(雙)回圈現在比您通常預期的要慢。

正如@Stephen Cannon 在評論中指出的那樣,這種對齊很可能會導致加載/存盤單元或快取中出現錯誤的別名我在谷歌上搜索了一下,發現英特爾實際上有一個用于部分地址別名停頓的硬體計數器:

http://software.intel.com/sites/products/documentation/doclib/stdxe/2013/~amplifierxe/pmw_dp/events/partial_address_alias.html


5 個區域 - 說明

區域 1:

這個很簡單。資料集非常小,性能主要由回圈和分支等開銷決定。

區域 2:

在這里,隨著資料大小的增加,相對開銷的數量下降并且性能“飽和”。這里的兩個回圈比較慢,因為它有兩倍的回圈和分支開銷。

我不確定這里到底發生了什么......對齊仍然可以發揮作用,因為 Agner Fog 提到了cache bank conflicts(該鏈接是關于 Sandy Bridge 的,但這個想法應該仍然適用于 Core 2。)

區域 3:

此時,資料不再適合 L1 快取。因此性能受到 L1 <-> L2 快取帶寬的限制。

區域 4:

單回圈中的性能下降是我們所觀察到的。并且如前所述,這是由于對齊(很可能)導致處理器加載/存盤單元中的假別名停頓。

然而,為了發生假別名,資料集之間必須有足夠大的步幅。這就是為什么您在區域 3 中看不到它。

區域 5:

此時,快取中沒有任何內容。所以你受到記憶體帶寬的限制。


為什么在單獨的回圈中按元素添加比在組合回圈中快得多? 為什么在單獨的回圈中按元素添加比在組合回圈中快得多? 為什么在單獨的回圈中按元素添加比在組合回圈中快得多?

uj5u.com熱心網友回復:

好的,正確的答案肯定與 CPU 快取有關。但是使用快取引數可能會非常困難,尤其是在沒有資料的情況下。

有很多答案,引發了很多討論,但讓我們面對現實吧:快取問題可能非常復雜,而且不是一維的。它們在很大程度上取決于資料的大小,所以我的問題是不公平的:事實證明它位于快取圖中一個非常有趣的點。

@Mysticial 的回答說服了很多人(包括我),可能是因為它是唯一一個似乎依賴于事實的答案,但它只是真相的一個“資料點”。

這就是為什么我結合了他的測驗(使用連續分配與單獨分配)和@James'Answer 的建議。

下圖顯示,根據具體場景和使用的引數,大多數答案,尤其是對問題和答案的大多數評論可以被認為是完全錯誤的或正確的。

請注意,我最初的問題是在n = 100.000處。這一點(偶然)表現出特殊行為:

  1. 它在一個和兩個回圈版本之間具有最大的差異(幾乎是三倍)

  2. 這是唯一一點,單回圈(即連續分配)勝過雙回圈版本。(這使得 Mysticial 的回答成為可能。)

使用初始化資料的結果:

為什么在單獨的回圈中按元素添加比在組合回圈中快得多?

使用未初始化資料的結果(這是 Mysticial 測驗的結果):

為什么在單獨的回圈中按元素添加比在組合回圈中快得多?

這是一個難以解釋的資料:初始化資料,分配一次并重復用于以下不同向量大小的每個測驗用例:

為什么在單獨的回圈中按元素添加比在組合回圈中快得多?

提議

應該要求 Stack Overflow 上的每個低級性能相關問題提供整個快取相關資料大小范圍的 MFLOPS 資訊!在沒有這些資訊的情況下思考答案,尤其是與他人討論答案,是在浪費每個人的時間。

uj5u.com熱心網友回復:

第二個回圈涉及的快取活動要少得多,因此處理器更容易跟上記憶體需求。

uj5u.com熱心網友回復:

想象一下,您正在一臺機器上作業,該機器n的正確值只能一次在記憶體中保存兩個陣列,但是通過磁盤快取的可用總記憶體仍然足以容納所有四個陣列。

假設一個簡單的后進先出快取策略,這段代碼:

for(int j=0;j<n;j  ){
    a[j]  = b[j];
}
for(int j=0;j<n;j  ){
    c[j]  = d[j];
}

將首先導致ab加載到 RAM 中,然后完全在 RAM 中進行處理。當第二個回圈開始時,c然后d將其從磁盤加載到 RAM 中并繼續運行。

另一個回圈

for(int j=0;j<n;j  ){
    a[j]  = b[j];
    c[j]  = d[j];
}

每次回圈時都會調出兩個陣列并調入另外兩個陣列這顯然會慢得多

您可能沒有在測驗中看到磁盤快取,但您可能看到了一些其他形式的快取的副作用。


這里似乎有點混亂/誤解,所以我會嘗試用一個例子來詳細說明。

假設n = 2我們正在處理位元組。在我的場景中,我們只有 4 個位元組的 RAM,而我們的其余記憶體明顯更慢(比如訪問時間長 100 倍)。

假設一個相當愚蠢的快取策略,如果位元組不在快取中,將它放在那里并在我們處理它時也獲取以下位元組,您將得到類似這樣的場景:

  • for(int j=0;j<n;j  ){
     a[j]  = b[j];
    }
    for(int j=0;j<n;j  ){
     c[j]  = d[j];
    }
    
  • cachea[0]然后and設定在快取a[1]- 現在快取中有四個位元組,并且. 成本 = 100 100。b[0]b[1]a[0] = a[0] b[0]a[0], a[1]b[0], b[1]

  • 設定a[1] = a[1] b[1]在快取中。成本 = 1 1。
  • 重復cd
  • 總成本 =(100 100 1 1) * 2 = 404

  • for(int j=0;j<n;j  ){
     a[j]  = b[j];
     c[j]  = d[j];
    }
    
  • cachea[0]然后and設定在快取a[1]- 現在快取中有四個位元組,并且. 成本 = 100 100。b[0]b[1]a[0] = a[0] b[0]a[0], a[1]b[0], b[1]

  • a[0], a[1], b[0], b[1]從快取和快取彈出c[0]c[1]然后在快取中設定成本 = 100 100。d[0]d[1]c[0] = c[0] d[0]
  • 我懷疑你開始明白我要去哪里了。
  • 總成本 =(100 100 100 100) * 2 = 800

這是一個典型的快取抖動場景。

uj5u.com熱心網友回復:

這不是因為不同的代碼,而是因為快取:RAM 比 CPU 暫存器慢,CPU 內部有一個快取記憶體,以避免每次變數發生變化時都寫入 RAM。但是快取不像 RAM 那樣大,因此它只映射其中的一小部分。

第一個代碼在每個回圈中交替修改遠程記憶體地址,因此需要不斷地使快取無效。

第二個代碼不交替:它只是在相鄰地址上流動兩次。這使得所有作業都在快取中完成,只有在第二個回圈開始后才使它無效。

uj5u.com熱心網友回復:

我無法復制這里討論的結果。

我不知道是否應該歸咎于糟糕的基準代碼,或者是什么,但這兩種方法在我的機器上使用以下代碼彼此相差在 10% 以內,并且一個回圈通常只比兩個快一點 - 正如你預計。

陣列大小從 2^16 到 2^24,使用八個回圈。我很小心地初始化了源陣列,因此 =分配不會要求FPU添加解釋為雙精度的記憶體垃圾。

我試過各種方案,例如將 , 的賦值b[j]放在d[j]回圈InitToZero[j]內部,以及使用 = b[j] = 1and = d[j] = 1,我得到了相當一致的結果。

正如您所料,初始化bd在回圈內使用為組合方法提供了優勢,因為它們是在分配給InitToZero[j]之前背靠背完成的,但仍在 10% 以內。去搞清楚。ac

硬體是Dell XPS 8500,配備第 3 代Core i7 @ 3.4 GHz 和 8 GB 記憶體。對于 2^16 到 2^24,使用八個回圈,累計時間分別為 44.987 和 40.965。Visual C 2010,完全優化。

PS:我將回圈更改為倒數到零,組合方法稍微快一些。撓我的頭。請注意新的陣列大小和回圈計數。

// MemBufferMystery.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include <iostream>
#include <cmath>
#include <string>
#include <time.h>

#define  dbl    double
#define  MAX_ARRAY_SZ    262145    //16777216    // AKA (2^24)
#define  STEP_SZ           1024    //   65536    // AKA (2^16)

int _tmain(int argc, _TCHAR* argv[]) {
    long i, j, ArraySz = 0,  LoopKnt = 1024;
    time_t start, Cumulative_Combined = 0, Cumulative_Separate = 0;
    dbl *a = NULL, *b = NULL, *c = NULL, *d = NULL, *InitToOnes = NULL;

    a = (dbl *)calloc( MAX_ARRAY_SZ, sizeof(dbl));
    b = (dbl *)calloc( MAX_ARRAY_SZ, sizeof(dbl));
    c = (dbl *)calloc( MAX_ARRAY_SZ, sizeof(dbl));
    d = (dbl *)calloc( MAX_ARRAY_SZ, sizeof(dbl));
    InitToOnes = (dbl *)calloc( MAX_ARRAY_SZ, sizeof(dbl));
    // Initialize array to 1.0 second.
    for(j = 0; j< MAX_ARRAY_SZ; j  ) {
        InitToOnes[j] = 1.0;
    }

    // Increase size of arrays and time
    for(ArraySz = STEP_SZ; ArraySz<MAX_ARRAY_SZ; ArraySz  = STEP_SZ) {
        a = (dbl *)realloc(a, ArraySz * sizeof(dbl));
        b = (dbl *)realloc(b, ArraySz * sizeof(dbl));
        c = (dbl *)realloc(c, ArraySz * sizeof(dbl));
        d = (dbl *)realloc(d, ArraySz * sizeof(dbl));
        // Outside the timing loop, initialize
        // b and d arrays to 1.0 sec for consistent  = performance.
        memcpy((void *)b, (void *)InitToOnes, ArraySz * sizeof(dbl));
        memcpy((void *)d, (void *)InitToOnes, ArraySz * sizeof(dbl));

        start = clock();
        for(i = LoopKnt; i; i--) {
            for(j = ArraySz; j; j--) {
                a[j]  = b[j];
                c[j]  = d[j];
            }
        }
        Cumulative_Combined  = (clock()-start);
        printf("\n %6i miliseconds for combined array sizes %i and %i loops",
                (int)(clock()-start), ArraySz, LoopKnt);
        start = clock();
        for(i = LoopKnt; i; i--) {
            for(j = ArraySz; j; j--) {
                a[j]  = b[j];
            }
            for(j = ArraySz; j; j--) {
                c[j]  = d[j];
            }
        }
        Cumulative_Separate  = (clock()-start);
        printf("\n %6i miliseconds for separate array sizes %i and %i loops \n",
                (int)(clock()-start), ArraySz, LoopKnt);
    }
    printf("\n Cumulative combined array processing took .3f seconds",
            (dbl)(Cumulative_Combined/(dbl)CLOCKS_PER_SEC));
    printf("\n Cumulative seperate array processing took .3f seconds",
        (dbl)(Cumulative_Separate/(dbl)CLOCKS_PER_SEC));
    getchar();

    free(a); free(b); free(c); free(d); free(InitToOnes);
    return 0;
}

我不確定為什么決定 MFLOPS 是一個相關指標。雖然我的想法是專注于記憶體訪問,但我試圖盡量減少浮點計算時間。我離開了 =,但我不確定為什么。

沒有計算的直接分配將是記憶體訪問時間的更清晰的測驗,并且將創建一個與回圈計數無關的統一測驗。也許我在談話中漏掉了什么,但值得三思。如果加號被排除在分配之外,累積時間幾乎相同,均為 31 秒。

uj5u.com熱心網友回復:

這是因為 CPU 沒有那么多快取未命中(它必須等待陣列資料來自 RAM 芯片)。連續調整陣列的大小以超過1 級快取(L1) 的大小,然后超過 CPU 的2 級快取(L2) 的大小,并繪制代碼所用的時間對您來說很有趣根據陣列的大小執行。該圖不應像您期望的那樣是一條直線。

uj5u.com熱心網友回復:

第一個回圈交替寫入每個變數。第二個和第三個僅對元素大小進行小幅跳躍。

嘗試用筆和紙寫兩條平行線,每條 20 個十字,間隔 20 厘米。嘗試一次完成一行,然后完成另一行,然后通過在每行中交替寫十字來嘗試另一次。

uj5u.com熱心網友回復:

原始問題

為什么一個回圈比兩個回圈慢這么多?


結論:

案例 1是一個經典的插值問題,但恰好是一個低效問題。我還認為這是許多機器架構和開發人員最終構建和設計能夠執行多執行緒應用程式和并行編程的多核系統的主要原因之一。

從這種方法來看,不涉及硬體、作業系統和編譯器如何協同作業以進行涉及 RAM、快取、頁面檔案等的堆分配;作為這些演算法基礎的數學向我們展示了這兩者中哪一個是更好的解決方案。

我們可以使用一個Boss存在 a的類比,Summation它代表For Loop必須在工人A&之間移動的 a B

我們可以很容易地看到案例 2至少是案例1的一半快,因為需要移動的距離和工人之間花費的時間不同。這個數學幾乎與基準時間以及匯編指令中的差異數量幾乎完全吻合。


我現在將在下面開始解釋所有這些是如何作業的。


評估問題

OP的代碼:

const int n=100000;

for(int j=0;j<n;j  ){
    a1[j]  = b1[j];
    c1[j]  = d1[j];
}

for(int j=0;j<n;j  ){
    a1[j]  = b1[j];
}
for(int j=0;j<n;j  ){
    c1[j]  = d1[j];
}

考慮

考慮到 OP 關于for回圈的兩種變體的原始問題和他對快取行為的修改后的問題以及許多其他優秀的答案和有用的評論;我想通過對這種情況和問題采取不同的方法來嘗試做一些不同的事情。


該方法

考慮到這兩個回圈以及所有關于快取和頁面歸檔的討論,我想采用另一種方法從不同的角度來看待這個問題。一種不涉及快取和頁面檔案,也不涉及分配記憶體的執行,事實上,這種方法甚至根本不涉及實際的硬體或軟體。


透視

在查看代碼一段時間后,很明顯問題是什么以及產生問題的原因。讓我們將其分解為一個演算法問題,并從使用數學符號的角度來看它,然后對數學問題和演算法進行類比。


我們所知道的

我們知道這個回圈將運行 100,000 次。我們還知道a1, b1, c1&d1是 64 位架構上的指標。在 32 位機器上的 C 中,所有指標都是 4 個位元組,而在 64 位機器上,它們的大小是 8 個位元組,因為指標的長度是固定的。

我們知道在這兩種情況下我們都有 32 個位元組要分配給它。唯一的區別是我們在每次迭代中分配 32 個位元組或兩組 2-8 位元組,其中第二種情況我們為兩個獨立回圈的每次迭代分配 16 個位元組。

兩個回圈在總分配中仍然等于 32 位元組。有了這些資訊,現在讓我們繼續展示這些概念的一般數學、演算法和類比。

我們確實知道在這兩種情況下必須執行的同一組或一組操作的次數。我們確實知道在這兩種情況下需要分配的記憶體量。我們可以評估這兩種情況之間分配的總體作業量將大致相同。


我們不知道的

我們不知道每種情況需要多長時間,除非我們設定計數器并運行基準測驗。但是,基準已經包含在原始問題以及一些答案和評論中;我們可以看到兩者之間存在顯著差異,這就是針對此問題提出此建議的全部原因。


讓我們調查一下

很明顯,許多人已經通過查看堆分配、基準測驗、查看 RAM、快取和頁面檔案來做到這一點。查看特定的資料點和特定的迭代指數也包括在內,關于這個特定問題的各種對話讓很多人開始質疑與它相關的其他事情。我們如何開始使用數學演算法并對其進行類比來看待這個問題?我們首先做出幾個斷言!然后我們從那里構建我們的演算法。


我們的斷言:

  • 我們將讓我們的回圈及其迭代成為一個從 1 開始到 100000 結束的求和,而不是像在回圈中那樣從 0 開始,因為我們不需要擔心記憶體尋址的 0 索引方案,因為我們只對演算法本身。
  • 在這兩種情況下,我們都有四個函式和兩個函式呼叫,每個函式呼叫執行兩個操作。我們將把它們設定為函式和函式呼叫,如下所示F1()F2()f(a)f(b)f(c)f(d)

演算法:

第一種情況: - 只有一個求和,但有兩個獨立的函式呼叫。

Sum n=1 : [1,100000] = F1(), F2();
                       F1() = { f(a) = f(a)   f(b); }
                       F2() = { f(c) = f(c)   f(d); }

第二種情況: - 兩個求和,但每個都有自己的函式呼叫。

Sum1 n=1 : [1,100000] = F1();
                        F1() = { f(a) = f(a)   f(b); }

Sum2 n=1 : [1,100000] = F1();
                        F1() = { f(c) = f(c)   f(d); }

如果您注意到F2()只存在于Sumfrom Case1whereF1()包含在SumfromCase1和 in bothSum1Sum2from 中Case2稍后當我們開始斷定第二個演算法中正在發生優化時,這一點就會很明顯。

通過第一個 caseSum呼叫的迭代f(a)將添加到其自身f(b),然后它呼叫f(c)將執行相同的操作但f(d)為每次100000迭代添加到自身。在第二種情況下,我們有Sum1并且Sum2兩者的行為相同,就好像它們是連續呼叫兩次的同一個函式一樣。

在這種情況下,我們可以將Sum1andSum2視為普通的舊SumwhereSum在這種情況下看起來像這樣:Sum n=1 : [1,100000] { f(a) = f(a) f(b); }現在這看起來像是一種優化,我們可以將它視為相同的功能。


類比總結

對于我們在第二種情況下看到的情況,它幾乎看起來好像存在優化,因為兩個 for 回圈具有完全相同的簽名,但這不是真正的問題。問題不在于f(a)f(b)f(c)和所做的作業f(d)在這兩種情況下以及兩者之間的比較中,求和在每種情況下必須行進的距離的差異會導致執行時間的差異。

for回圈視為對迭代進行的求和Boss,即向兩個人A&發出命令,B并且他們的作業分別是肉C&D并從他們那里拿起一些包裹并將其退回。在這個類比中,for回圈或求和迭代和條件檢查本身實際上并不代表Boss. 實際表示的Boss不是直接來自實際的數學演算法,而是來自例程或子例程、方法、函式、翻譯單元等的實際概念ScopeCode Block第一個演算法有一個范圍,而第二個演算法有兩個連續的范圍。

在每個呼叫單的第一個案例中,BossA并給出訂單并A去獲取B's包裹然后BossC并給出訂單來做同樣的事情并D在每次迭代中接收包裹。

在第二種情況下,Boss直接使用A去獲取B's包,直到收到所有包。然后Bossworks withC做同樣的事情來獲取所有的D's包。

由于我們正在使用 8 位元組指標并處理堆分配,因此讓我們考慮以下問題。假設Boss距離 100 英尺,距離A500A英尺C由于執行的順序,我們不需要擔心Boss最初有多遠。C在這兩種情況下,Boss最初都是從Afirst 然后到B這個類比并不是說這個距離是精確的。它只是一個有用的測驗用例場景,可以展示演算法的作業原理。

在許多情況下,在進行堆分配以及使用快取和頁面檔案時,地址位置之間的這些距離可能不會有太大變化,或者可能會有很大差異,具體取決于資料型別的性質和陣列大小。


測驗用例:

第一個案例:在第一次迭代中,Boss他必須先走 100 英尺才能將訂單交給AA然后離開并做他的事情,但隨后他Boss必須走 500 英尺才能C將他的訂單交給他。然后在下一次迭代和每次迭代之后Boss必須在兩者之間來回移動 500 英尺。

第二種情況:必須在第Boss一次迭代中行進 100 英尺到達A,但在那之后,他已經在那里并等待A回傳,直到所有單據都填滿。然后Boss必須在第一次迭代中行進 500 英尺,C因為C是 500 英尺A由于這Boss( Summation, For Loop )是在與A他合作后立即呼叫的,因此他就像他所做的那樣在那里等待,A直到完成所有C's訂單單。


行進距離的差異

const n = 100000
distTraveledOfFirst = (100   500)   ((n-1)*(500   500));
// Simplify
distTraveledOfFirst = 600   (99999*1000);
distTraveledOfFirst = 600   99999000;
distTraveledOfFirst = 99999600
// Distance Traveled On First Algorithm = 99,999,600ft

distTraveledOfSecond = 100   500 = 600;
// Distance Traveled On Second Algorithm = 600ft;

任意值的比較

我們很容易看出,600 遠遠小于大約 1 億。現在,這并不準確,因為我們不知道每次迭代中每次呼叫的 RAM 地址或快取或頁面檔案之間的實際距離差異將歸因于許多其他看不見的變數。這只是對要了解的情況的評估,并從最壞的情況來看。

從這些數字看來,演算法似乎應該是99%演算法二慢;然而,這只是Boss's演算法的一部分或責任,它不考慮實際的作業人員A, B, C, &D以及他們在回圈的每次迭代中必須做的事情。所以老板的作業只占全部作業的15-40%左右。通過工人完成的大部分作業對將速度差的比率保持在大約 50-70% 的影響略大


觀察: -兩種演算法之間的差異

In this situation, it is the structure of the process of the work being done. It goes to show that Case 2 is more efficient from both the partial optimization of having a similar function declaration and definition where it is only the variables that differ by name and the distance traveled.

我們還看到案例 1中的總行進距離比案例 2中的要遠得多,我們可以將這個距離視為我們的時間因子 between the two algorithms. Case 1 has considerable more work to do than Case 2 does.

這可以從兩種情況下顯示的匯編指令的證據中看出。連同已經說明的這些案例,這并沒有說明在案例 1中老板將不得不等待A&C to get back before he can go back to A again for each iteration. It also doesn't account for the fact that if A or B is taking an extremely long time then both the Boss and the other worker(s) are idle waiting to be executed.

情況2 the only one being idle is the Boss until the worker gets back. So even this has an impact on the algorithm.



The OP's Amended Question(s)

編輯:事實證明這個問題無關緊要,因為行為嚴重取決于陣列 (n) 的大小和 CPU 快取。因此,如果有進一步的興趣,我改寫這個問題:

您能否對導致不同快取行為的細節提供一些可靠的見解,如下圖中的五個區域所示?

通過為這些 CPU 提供類似的圖表來指出 CPU/快取架構之間的差異可能也很有趣。


Regarding These Questions

正如我毫無疑問地證明的那樣,甚至在涉及硬體和軟體之前就存在一個潛在的問題。

現在關于記憶體和快取的管理以及頁面檔案等,它們都在以下系統的集成系統中協同作業:

  • 架構 (hardware, firmware, some embedded drivers, kernels and assembly instruction sets).
  • 作業系統 (file and memory management systems, drivers and the registry).
  • 編譯器 (translation units and optimizations of the source code).
  • 甚至源代碼本身及其獨特的演算法集。

與第二種演算法相比,我們甚至在將第一種演算法應用于具有任意體系結構作業系統可編程語言的任何機器之前,就已經可以看到第一種演算法中存在瓶頸。在涉及現代計算機的內在函式之前已經存在一個問題。


最終結果

然而; 這并不是說這些新問題不重要,因為它們本身很重要,而且它們確實發揮了作用。它們確實會影響程式和整體性能,并且從許多給出答案和/或評論的人的各種圖表和評估中可以明顯看出這一點。

如果您注意了Boss和兩個工人A&的類比,B他們必須分別從C&那里取回包裹,D并考慮所討論的兩種演算法的數學符號;你可以看到沒有計算機硬體和軟體的參與Case 2大約60%Case 1

當您在將這些演算法應用于某些源代碼、編譯、優化并通過作業系統執行以在給定硬體上執行它們的操作后查看圖形和圖表時,您甚至可以看到差異之間的更多降級在這些演算法中。

如果Data集合相當小,一開始看起來可能并沒有那么糟糕。然而,由于Case 1我們可以根據時間執行差異來查看此函式的增長60 - 70%要慢:Case 2

DeltaTimeDifference approximately = Loop1(time) - Loop2(time)
//where
Loop1(time) = Loop2(time)   (Loop2(time)*[0.6,0.7]) // approximately
// So when we substitute this back into the difference equation we end up with
DeltaTimeDifference approximately = (Loop2(time)   (Loop2(time)*[0.6,0.7])) - Loop2(time)
// And finally we can simplify this to
DeltaTimeDifference approximately = [0.6,0.7]*Loop2(time)

該近似值是這兩個回圈在演算法和涉及軟體優化和機器指令的機器操作方面的平均差異。

當資料集呈線性增長時,兩者之間的時間差也呈線性增長。演算法 1 比演算法 2 有更多的獲取,這在第一次迭代后每次迭代Boss必須來回移動A&之間的最大距離時很明顯,而演算法 2必須移動一次,然后在完成后他必須移動從的最大距離只有一次CBossAAAC

試圖同時Boss專注于做兩件類似的事情并來回兼顧,而不是專注于類似的連續任務,這會讓他在一天結束時非常生氣,因為他不得不出差和作業兩倍。因此,不要讓你的老板陷入內插瓶頸,因為老板的配偶和孩子不會感激,從而失去情況的范圍。



修訂:軟體工程設計原則

--迭代回圈中本地堆疊堆分配計算之間的區別以及它們的用法、效率和有效性之間的區別 --

我上面提出的數學演算法主要適用于對堆上分配的資料執行操作的回圈。

  • 連續堆疊操作:
    • 如果回圈在堆疊幀內的單個代碼塊或范圍內對本地資料執行操作,它仍然會有點適用,但記憶體位置更接近它們通常是連續的,并且行進距離或執行時間不同幾乎可以忽略不計。由于堆內沒有進行分配,因此記憶體不會分散,也不會通過 ram 獲取記憶體。記憶體通常是順序的,并且相對于堆疊幀和堆疊指標。
  • 當在堆疊上進行連續操作時,現代處理器將快取重復值和地址,將這些值保存在本地快取暫存器中。這里的操作或指令的時間是納秒級的。
  • 連續的堆分配操作:
    • 當您開始應用堆分配并且處理器必須在連續呼叫中獲取記憶體地址時,根據 CPU、總線控制器和 RAM 模塊的體系結構,操作或執行的時間可能在微到毫秒。與快取堆疊操作相比,這些操作非常慢。
    • CPU 將不得不從 RAM 中獲取記憶體地址,并且與 CPU 本身的內部資料路徑或資料總線相比,系統總線上的任何內容通常都很慢。

So when you are working with data that needs to be on the heap and you are traversing through them in loops, it is more efficient to keep each data set and its corresponding algorithms within its own single loop. You will get better optimizations compared to trying to factor out consecutive loops by putting multiple operations of different data sets that are on the heap into a single loop.

It is okay to do this with data that is on the stack since they are frequently cached, but not for data that has to have its memory address queried every iteration.

This is where software engineering and software architecture design comes into play. It is the ability to know how to organize your data, knowing when to cache your data, knowing when to allocate your data on the heap, knowing how to design and implement your algorithms, and knowing when and where to call them.

You might have the same algorithm that pertains to the same data set, but you might want one implementation design for its stack variant and another for its heap-allocated variant just because of the above issue that is seen from its O(n) complexity of the algorithm when working with the heap.

根據我多年來的觀察,很多人沒有考慮到這個事實。他們將傾向于設計一種適用于特定資料集的演算法,并且無論資料集是本地快取在堆疊上還是分配在堆上,他們都會使用它。

如果你想要真正的優化,是的,它可能看起來像是代碼重復,但概括地說,擁有同一演算法的兩個變體會更有效。一個用于堆疊操作,另一個用于在迭代回圈中執行的堆操作!

這是一個偽示例:兩個簡單的結構,一個演算法。

struct A {
    int data;
    A() : data{0}{}
    A(int a) : data{a}{}
};
struct B {
    int data;
    B() : data{0}{}
    A(int b) : data{b}{}
}

template<typename T>
void Foo( T& t ) {
    // Do something with t
}

// Some looping operation: first stack then heap.

// Stack data:
A dataSetA[10] = {};
B dataSetB[10] = {};

// For stack operations this is okay and efficient
for (int i = 0; i < 10; i   ) {
   Foo(dataSetA[i]);
   Foo(dataSetB[i]);
}

// If the above two were on the heap then performing
// the same algorithm to both within the same loop
// will create that bottleneck
A* dataSetA = new [] A();
B* dataSetB = new [] B();
for ( int i = 0; i < 10; i   ) {
    Foo(dataSetA[i]); // dataSetA is on the heap here
    Foo(dataSetB[i]); // dataSetB is on the heap here
} // this will be inefficient.

// To improve the efficiency above, put them into separate loops...

for (int i = 0; i < 10; i   ) {
    Foo(dataSetA[i]);
}
for (int i = 0; i < 10; i   ) {
    Foo(dataSetB[i]);
}
// This will be much more efficient than above.
// The code isn't perfect syntax, it's only pseudo code
// to illustrate a point.

這就是我所指的堆疊變體與堆變體的單獨實作。演算法本身并不重要,重要的是您將在其中使用它們的回圈結構。

uj5u.com熱心網友回復:

它可能是舊的 C 和優化。在我的電腦上,我獲得了幾乎相同的速度:

一個回圈:1.577 ms

兩個回圈:1.507 毫秒

我在配備 16 GB RAM 的 E5-1620 3.5 GHz 處理器上運行 Visual Studio 2015。

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/534989.html

標籤:Intel Collective C 表现x86矢量化编译器优化

上一篇:為什么處理排序陣列比處理未排序陣列更快?

下一篇:運算子多載的基本規則和習慣用法是什么?

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:20:47 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:20:25 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:20:17 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:20:10 more
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:19:44 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:19:07 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:18:57 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:18:49 more
  • 05單件模式

    #經典的單件模式 public class Singleton { private static Singleton uniqueInstance; //一個靜態變數持有Singleton類的唯一實體。 // 其他有用的實體變數寫在這里 //構造器宣告為私有,只有Singleton可以實體化這個類! ......

    uj5u.com 2023-04-19 08:42:51 more
  • 【架構與設計】常見微服務分層架構的區別和落地實踐

    軟體工程的方方面面都遵循一個最基本的道理:沒有銀彈,架構分層模型更是如此,每一種都有各自優缺點,所以請根據不同的業務場景,并遵循簡單、可演進這兩個重要的架構原則選擇合適的架構分層模型即可。 ......

    uj5u.com 2023-04-19 08:42:41 more