Niyə ayrı dövrlərdə stiqma takviyeleri kombinə edilmiş dövrə nisbətən daha sürətli?

Yığın yaddaşına a1 , b1 , c1d1 nöqtəsi d1 və mənim rəqəmli d1 aşağıdakı əsas loop var.

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

Bu döngə 10,000 dəfə başqa bir döngü ilə həyata keçirilir. Sürətləndirmək üçün kodunu dəyişdim:

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

Intel Core 2 Duo (x64) 32 bit üçün tam optimallaşdırma və SSE2 effektivliyi ilə MS Visual C ++ 10.0-da tərtib edilmiş ilk nümunə 5.5 saniyə çəkir və ikiqat dövrü olan nümunə yalnız 1,9 saniyə çəkir. Mənim sualım: (Xahiş edirəm, aşağıda göstərilən sualımı nəzərdən keçirin)

PS: Bu kömək edə biləcəyinə əmin deyiləm:

İlk dövr üçün sökülmə əsasən bu kimi görünür (bu blok tam proqramda beş dəfə təkrarlanır):

 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] 

İki döngü nümunəsində hər bir döngə bu kodu yaradır (aşağıdakı blok üç dəfə təkrarlanır):

 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 

Sual, alakasız olub, çünki davranış, dizinin (n) və CPU önbelleğinin ölçüsündən çox asılıdır. Beləliklə, daha çox marağın olması halında, mən sualımı təkrarlayıram:

Aşağıdakı diaqramdakı beş sahədə göstərildiyi kimi müxtəlif cache davranışlarına gətirib çıxaran detallar barədə bir neçə ətraflı fikir verə bilərsinizmi?

CPU və cache arxitekturaları arasındakı fərqləri göstərmək maraqlı olardı, bu prosessorlar üçün oxşar cədvəl verildi.

PPS: burada tam kod. TBB Tick_Count istifadə Tick_Count ifadə etmədən aradan Tick_Count üçün daha yüksək bir qətnamə ilə sinxronizasiya etmək üçün istifadə edir:

 #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; } } 

(Müxtəlif dəyərlər üçün FLOP / s göstərir.)

2019

2073
17 дек. Johannes Gerer 17 dekabrda təyin olundu. 2011-12-17 23:40 '11 saat 23:40 'da 2011-12-17 23:40
@ 10 cavab

Bunun daha da təhlili əsasında, mən inanıram ki, bu (ən azı qismən) dörd göstəricinin uyğunlaşması ilə bağlıdır. Bu, bəzi cache / yol münaqişəsinə səbəb olacaq.

Sütunlarınızı necə ayırdığınızı düzgün başa düşsəniz, onlar çox güman ki səhifə dizesiyle hizalanacaqlar .

Bu, hər bir dövrədə olan bütün zənglərin eyni cache faylına düşəcəyini bildirir. Ancaq bir müddət Intel prosessorları 8 yollu L1 önbellek birləşməsinə sahib idi. Amma əslində performans tamamilə uniform deyil. 4 kanal kanallarına giriş hələ iki yoldan daha asandır.

EDIT: Əslində, bütün diziləri ayrı seçdiyinizə bənzəyir. Adətən, belə böyük ayırmalar tələb olunduqda, distribyutor OS-dan yeni səhifələr istəməlidir. Buna görə, böyük seçimlər səhifənin sərhədindən eyni ofset ilə nümayiş ediləcək yüksək ehtimalı var.

Test kodudur:

 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; } 

Test nəticələri:

EDIT: Core 2 Real Architecture haqqında nəticələr:

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 

sözlər:

  • Bir döngü ilə 6.206 saniyə və iki dövrə ilə 2.116 saniyə . Bu, OP-nin nəticələrini düzgün şəkildə əks etdirir.

  • İlk iki testdə, dizilər ayrı olaraq ayrılır. Onların hamısı səhifəyə nisbətdə eyni uyğunluğa sahib olduğunu görürsünüz.

  • İkinci iki testdə, bu ardıcıllığı qırmaq üçün dizilər birlikdə doludur. Burada hər iki dövrünün daha sürətli olduğunu görəcəksiniz. Bundan əlavə, ikincisi (ikiqat) dövrü indi ümumiyyətlə gözlədiyiniz kimi yavaş.

Şərhlər @ Stephen Cannon qeydləri kimi, bu hizalama yük / saxlama və ya önbellek baxımından saxta düzəldilməyə səbəb ola biləcək bir ehtimalı var. Mən bu barədə düşündüm və Intel həqiqətən qismən ünvanları düzəltmək üçün bir hardware counter edir ki:

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


border=0

5 Regionlar - Şərhlər

Region 1:

Bu asandır. Data seti belə kiçikdir ki, dövriyyə və dallaşdırma kimi yuxarı xərclər üstünlük təşkil edir.

Region 2:

Burada data ölçüsü artdıqda nisbi maneə miqdarı azalır və performans "doymuş" olur. Burada iki dövrə yavaş olur, çünki iki dəfədən artıq iplər və filiallar var.

Burada nə baş verdiyinə əmin deyiləm ... Alignment hələ də təsir edə bilər, çünki Agner Fog bank cache münaqişələrini xatırladır. (Bu link Sandy Körpüsünə aiddir, lakin fikir Core 2-ə tətbiq edilməlidir.)

Region 3:

Bu nöqtədə data artıq L1 önbelleğine sığmaz. Beləliklə, performans L1 ↔ L2 bant genişliyi ilə məhdudlaşır.

Region 4:

Bir dövrdə performansın azalması müşahidə etdiyimiz şeydir. Və artıq qeyd edildiyi kimi, bu proseslərin yükləmə / saxlama bloklarında saxta aliasların bloklanmasına səbəb olan hizalanma ilə bağlıdır.

Bununla yanaşı, saxta düzəltmənin baş verməsi üçün məlumat dəstləri arasında kifayət qədər böyük bir addım olmalıdır. Buna görə bölgədə bunu görmürəm 3.

Bölge 5:

Bu nöqtədə, heç bir şey önbelleğe sığmaz. Beləliklə, siz yaddaş bant genişliyi ilə bağlısınız.


border=0

2019

1585
18 дек. Cavab Mystic 18 dekabr verilir. 2011-12-18 00:17 '11 at 0:17 2011-12-18 00:17

Tamam, düzgün cavab prosessorun önbelleğiyle mütləq bir şey etməlidir. Ancaq cache argümanını istifadə edərək, xüsusilə məlumatsız olduqca mürəkkəb ola bilər.

Çox cavablar var ki, bu çox müzakirə aparır, amma bunlara baxın: önbellek problemləri çoxtərəfli və çox mürəkkəb ola bilər. Onlar məlumatın ölçüsünə çox asılıdırlar, buna görə sualım ədalətsiz idi: bu cache grafiğinde çox maraqlı oldu.

Mistik cavab bir çox insanları (mənim də daxil olmaqla) inandırdı, ehtimal ki, o, həqiqətə güvənən yeganə şəxs idi, ancaq həqiqətin yalnız bir "məlumat mənbəyi" idi.

Ona görə test (bir davamlı ya da ayrı bir dağılımı istifadə edərək) və cavab @ James Cavabı birləşdirdilər.

Aşağıdakı qrafiklər göstərir ki, cavabların əksəriyyəti və xüsusilə sual və cavablara verilən şərhlərin əksəriyyəti istifadə edilən xüsusi ssenariyə və parametrlərə əsasən tamamilə yanlış və ya doğru hesab edilə bilər.

İlk sualım n = 100.000 olduğunu unutmayın. Bu nöqtə (təsadüfən) xüsusi bir davranış nümayiş etdirir:

  • Bu, dövrünün bir və iki versiyası (demək olar ki, üç dəfə) arasında ən böyük uyğunsuzluqdur.

  • Bu, tək-loopun (yəni davamlı paylamanın) iki loop versiyasını aşdığı yeganə mənbəyidir. (Bu mistik cavab mümkün idi).

Başlanğıc məlumatları istifadə edərək nəticə:

border=0

2019

203
18 дек. Johannes Gerer tərəfindən verilmiş cavab 18 dekabr 2011-12-18 04:29 '11 at 4:29 2011-12-18 04:29

İkinci dövr daha az cache aktivliyini ehtiva edir, belə ki, prosessor yaddaş tələblərini saxlamaq üçün daha asandır.

69
17 дек. Cavab verildi Puppy Dec 17 2011-12-17 23:47 '11 saat 23:47 'də 2011-12-17 23:47

Təsəvvür edin ki, n -nun düzgün olduğu bir maşın üzərində işləyirsiniz, buna görə eyni vaxtda seriallarınızı iki yaddaşda saxlaya bilərsiniz, ancaq disklərin yığılması vasitəsi ilə mövcud yaddaşın ümumi həcmi hələ dördüncüsü saxlaya bilir.

Sadə LIFO caching siyasətini nəzərə alaraq, bu kod:

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

əvvəlcə RAM-yə yüklənmək üçün ab yə səbəb olacaq və sonra tamamilə RAM-yə işləyəcəkdir. İkinci dövr başlayanda cd diskdən RAM-a və işə yüklənir.

başqa dövr

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

döngünün ətrafında hər dəfə iki array və iki başlıqlı bir səhifə çıxacaqdır . Bu, açıq-aydın çox yavaş olacaq.

Yəqin testlərinizdə disklərin önbelleğini görmürsünüz, ancaq yəqin ki, başqa caching birləşməsinin yan təsirlərini görürsünüz.


Göründüyü kimi, burada bir az qarışıqlıq / yanlış anlaşma var, buna görə bir az nümunə ilə aydınlaşdırmağa çalışacağam.

n = 2 deyin və biz baytlarla işləyirik. Beləliklə, mənim ssenarimimizdə yalnız 4 bayt RAM varyaddaşın qalan hissəsi daha yavaşdır (məsələn, 100 dəfə daha çox çıxış).

Bir baytın önbellekte olmadığını düşünsəniz, orada saxlasanız və ondan sonra növbəti bayt əldə etsəniz, belə bir skript alacaqsınız:

  • İlə

     for(int j=0;j<n;j++){ a[j] += b[j]; } for(int j=0;j<n;j++){ c[j] += d[j]; } 
  • önbellekte a[0]a[1] sonra b[0]b[1] önbelleğine önbelleğe a[0] = a[0] + b[0] qoyduğumuzda, önbellekte dörd bayt, a[0], a[1]b[0], b[1] . Qiymət = 100 + 100.

  • Önbellekte a[1] = a[1] + b[1] ayarlayın. Xərc = 1 + 1.
  • cd üçün təkrarlayın.
  • Ümumi xərclər = (100 + 100 + 1 + 1) * 2 = 404

  • İlə

     for(int j=0;j<n;j++){ a[j] += b[j]; c[j] += d[j]; } 
  • önbellekte a[0]a[1] sonra b[0]b[1] önbelleğine önbelleğe a[0] = a[0] + b[0] qoyduğumuzda, önbellekte dörd bayt, a[0], a[1]b[0], b[1] . Qiymət = 100 + 100.

  • önbellekten c[0] ve c[1] d[0] ve d[1] a[0], a[1], b[0], b[1] kaldırın ve c[0] = c[0] + d[0] önbellekte. Qiymət = 100 + 100.
  • Şübhələnirəm ki, mən nereye gedirəm.
  • Toplam xərclər = (100 + 100 + 100 + 100) * 2 = 800

Bu klassik zibil qutusu ssenarisi.

41
18 дек. Cavab OldCurmudgeon tərəfindən verilir 18 dekabr. 2011-12-18 04:36 '11 'də 4:36' də 2011-12-18 04:36 'də

Bu fərqli bir kodla bağlı deyil, ancaq caching səbəbiylə: RAM prosessor qeydlərindən daha yavaş və yaddaş dəyişikliyi hər dəfə RAM yazmaqdan qaçmaq üçün CPU içərisindədir. Bununla belə, önbellek kiçikdir, çünki RAM bu səbəbdən yalnız bir hissəsini göstərir.

Birinci kod, uzaqdan yaddaşın ünvanlarını dəyişir və hər bir dövrədə dəyişir, beləliklə də daimi önbelleği silmək tələb olunur.

İkinci kod alternativ deyil: sadəcə iki dəfə bitişik ünvanlara keçir. Bu, bütün vəzifələrin cachedə tamamlanmasına səbəb olur, yalnız ikinci dövrü başladıqdan sonra onu ləğv edir.

29
17 дек. 17 dekabrda Emilio Garavaglia tərəfindən verilmiş cavab. 2011-12-17 23:49 '11 at 11:49 PM 2011-12-17 23:49

Burada müzakirə edilən nəticələri təkrar edə bilmərəm.

Səhv test kodunun günahlandırılması olub-olmadığını bilmirəm, amma bu iki üsul bir-birinin 10% -nə aşağıdakı kodunu istifadə edərək bir-birinin içindədir və bir döngə, ümumiyyətlə, iki dəfə bir az daha sürətli olur - gözlədiyiniz kimi.

Array ölçüsü səkkiz dövründən istifadə edərək 2 ^ 16-dan 2 ^ 24-ə qədər dəyişdi. Mən orijinal dizilərə başlamaq üçün diqqətli olundu ki, tapşırıq += FPU- nun yaddaş zibilini əlavə etməyini xahiş etmədi, cüt olaraq şərh edildi.

InitToZero[j] içərisində InitToZero[j] d[j] üçün b[j] , d[j] InitToZero[j] , həmçinin += b[j] = 1+= d[j] = 1 istifadə edərək, müxtəlif sxemlərlə InitToZero[j] və Mən olduqca ardıcıl nəticələr aldım.

Gözlənildiyi kimi, InitToZero[j] istifadə InitToZero[j] içərisində bd InitToZero[j] birləşdirilmiş yanaşma ac təyin etməzdən əvvəl yaxından aparıldığından, lakin hələ də 10% -də bir üstünlük təşkil etmişdir. Diqqət edin.

Hardware - 3 Core i7 @ 3.4 GHz prosessor və 8 GB yaddaş ilə Dell XPS 8500 . Səkkiz dövründən istifadə edərək, 2 ^ 16-dan 2 ^ 24 üçün ümumi vaxt 44.987 və 40.965 idi. Visual C ++ 2010 tam olaraq optimize edilmişdir.

PS: Geri sayım dövrlərini sıfıra dəyişdim və birləşdirilmiş üsul bir az daha sürətli idi. Başınızı çırpma Dizinin yeni ölçüsünə və dövrlərin sayına diqqət yetirin.

 // 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 %10.3f seconds", (dbl)(Cumulative_Combined/(dbl)CLOCKS_PER_SEC)); printf("\n Cumulative seperate array processing took %10.3f seconds", (dbl)(Cumulative_Separate/(dbl)CLOCKS_PER_SEC)); getchar(); free(a); free(b); free(c); free(d); free(InitToOnes); return 0; } 

MFLOPS-nun müvafiq göstəricinin niyə qərar verildiyindən əmin deyiləm. Fikir yaddaşa diqqət yönəldilməsinə baxmayaraq, float vaxtını minimuma endirməyə çalışdım. += Getdim, amma əmin deyiləm.

Hesaplamasız birbaşa tapşırıq, yaddaşa çatma müddətinin daha dəqiq bir sınağı olar və dövrün sayından asılı olmayaraq vahid olacaq bir test yaratacaqdır. Bəlkə söhbətdə bir şey qaçırdım, amma bu iki dəfə düşünməyə dəyər. Üstəgəl bir vəzifəyə daxil deyilsə, yığılma vaxtı demək olar ki, eynidır və 31 saniyədir.

18
30 дек. Cavab istifadəçi tərəfindən verilir1899861 30 dekabr. 2012-12-30 04:34 '12 at 4:34 2012-12-30 04:34

Bunun səbəbi, prosessorun bir çox önbellek misses (RAM çipləri gələn sıra məlumatları gözləmək lazımdır) yoxdur. Cədvəlin səviyyəsini 1 (L1) və sonra prosessorunuzun önbelleği 2 (L2) ölçüsünü aşmaq və kodu dizilərin ölçüsünə uyğun olaraq icra etmək üçün sərf olunan vaxt hesablamaq üçün dizilərin ölçüsünü hər zaman tənzimləmək maraqlı olardı. Gətir bir gözlədiyi kimi birbaşa olmamalıdır.

15
17 дек. Cümə günü cümə günü cavab verildi. 2011-12-17 23:52 '11 at 11:52 PM 2011-12-17 23:52

Birinci loop, hər dəyişənə girişini dəyişir. İkinci və üçüncü isə yalnız kiçik element ölçüsünü atır.

Qələm və 20 santimetr ilə ayrılmış bir hesabatı olan 20 xaçın iki paralel xəttini yazmağa çalışın. Bir dəfə və sonra başqa bir xəttini bitirməyə çalışın və hər xəttin növbə ilə xəttinə basaraq yenidən cəhd edin.

13
17 авг. Cavab Guillaume Kiz 17 avqustda verilir. 2012-08-17 18:23 '12 at 18:23 2012-08-17 18:23

Orijinal sual

Niyə bir dövr ikidən daha yavaş?


Nəticə:

Case 1 effektiv olmayan klassik interpolyasiya problemidir. Mən də düşünürəm ki, bu, bir çox maşın memarlığı və inkişaf etdiricilərinin multi-işıqlı proqramları və eyni zamanda paralel proqramlaşdırma işlətmə qabiliyyəti ilə çox çekirdekli sistemlərin yaradılması və dizaynını başa vurduqlarını əsas səbəblərdən biri hesab edirəm.

RAM, önbellek, disk belleği faylları və s. İlə işləyən yığını vurgulamak üçün donanım, OS və kompilyator (lar) ın necə işlədiyini təsəvvür etmədən bu yanaşmanı nəzərə alaraq; Bu alqoritmlərin əsasını təşkil edən riyaziyyat bizə bu iki variantdan ən yaxşı həllin hansı olduğunu göstərir. AB işçiləri arasında hərəkət etməsi lazım olan bir For Loop olan Boss və ya Summation , analojiyanı istifadə edə bilərik ki, bu işi 2 ən azı 1/2 , asanlıqla, bir az daha sürətli, işi 1 səfər üçün tələb olunan məsafə və işçilər arasında keçirdiyi vaxtdan görə fərqlənir. Bu riyaziyyat demək olar ki, virtual və mükəmməl Bench Mark Times həm montaj talimatları fərq ilə uyğun gəlir.

İndi bütün bunların necə işlədiyini izah etməyə başlayacağam.


Problemin qiymətləndirilməsi

OP kodu:

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

Həm də

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

Diqqət

Рассматривая оригинальный вопрос OP о двух вариантах циклов for и его исправленный вопрос о поведении кэшей, а также множество других превосходных @ и полезных комментариев; Я хотел бы попытаться сделать что-то другое здесь, используя другой подход к этой ситуации и проблеме.


Подход

Учитывая два цикла и все дискуссии о кэшировании и хранении страниц, я бы хотел использовать другой подход, чтобы взглянуть на это с другой точки зрения. Тот, который не включает кеш и файлы подкачки, ни выполнения для выделения памяти, на самом деле этот подход вообще не касается реального оборудования или программного обеспечения.


Перспектива

Посмотрев некоторое время на код, стало совершенно очевидно, в чем проблема и что ее генерирует. Давайте разберем это в алгоритмической задаче и рассмотрим ее с точки зрения использования математических обозначений, а затем применим аналогию к математическим задачам, а также к алгоритмам.


Что мы знаем

Мы знаем, что его цикл будет выполняться 100 000 раз. Мы также знаем, что a1 , b1 , c1 и d1 являются указателями на 64-битную архитектуру. В C++ на 32-битной машине все указатели имеют размер 4 байта, а на 64-битной машине они имеют размер 8 байтов, поскольку указатели имеют фиксированную длину. Мы знаем, что у нас есть 32 байта для выделения в обоих случаях. Единственное отличие состоит в том, что мы выделяем 32 байта или 2 набора по 2-8 байт на каждую итерацию, тогда как во 2-м случае мы выделяем 16 байтов для каждой итерации для обоих независимых циклов. Таким образом, оба цикла по-прежнему равны 32 байта в общем распределении. Получив эту информацию, давайте продолжим и покажем общую математику, алгоритм и аналогию. Мы знаем, сколько раз один и тот же набор или группа операций должны быть выполнены в обоих случаях. Мы знаем объем памяти, который должен быть выделен в обоих случаях. Мы можем оценить, что общая рабочая нагрузка распределений между обоими случаями будет примерно одинаковой.


Что мы не знаем

Мы не знаем, сколько времени это займет для каждого случая, если только мы не установим счетчик и не проведем тест. Однако контрольные показатели уже были включены в исходный вопрос, а также в некоторые ответы и комментарии, и мы можем видеть существенную разницу между этими двумя вопросами, и в этом заключается полное обоснование этого вопроса для этой проблемы и для ответа на него в начинается с.


Пусть расследовать

Уже очевидно, что многие уже сделали это, взглянув на распределение кучи, тесты производительности, на RAM, Cache и Page Files. Рассмотрение конкретных точек данных и конкретных итерационных индексов также было включено, и различные разговоры об этой конкретной проблеме заставили многих людей начать сомневаться в других связанных с этим вещах. Итак, как нам начать смотреть на эту проблему, используя математические алгоритмы и применяя к ней аналогию? Начнем с того, что сделаем пару утверждений! Затем мы строим наш алгоритм оттуда.


Наши утверждения:

  • Мы позволим нашему циклу и его итерациям быть суммированием, которое начинается с 1 и заканчивается на 100000 вместо того, чтобы начинаться с 0, как в циклах, поскольку нам не нужно беспокоиться о схеме индексации адресации памяти 0, так как нас просто интересует сам алгоритм.
  • В обоих случаях у нас есть 4 функции для работы и 2 вызова функций с 2 операциями, выполняемыми для каждого вызова функции. Таким образом, мы установим их как функции и вызовы функций, чтобы быть F1() , F2() , f(a) , f(b) , f(c) и f(d) .

Алгоритмы:

1-й случай: - только одно суммирование, но два независимых вызова функций.

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

2-й случай: - Два суммирования, но у каждого свой вызов функции.

 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() существует только в Sum где и Sum1 и Sum2 содержат только F1() . Это также станет очевидным позже, когда мы начнем делать вывод, что со вторым алгоритмом происходит своего рода оптимизация.

Итерации в первом случае Sum вызывает f(a) , который прибавит к самому себе f(b) затем вызовет f(c) , который сделает то же самое, но добавит f(d) к себе для каждых 100000 iterations . Во втором случае мы имеем Sum1 и Sum2 и оба действуют одинаково, как если бы они были одной и той же функцией, вызываемой дважды подряд. В этом случае мы можем рассматривать Sum1 и Sum2 как просто старую Sum2 Sum где Sum в этом случае выглядит следующим образом: Sum n=1: [1,100000] { f(a) = f(a) + f(b); } Sum n=1: [1,100000] { f(a) = f(a) + f(b); } и теперь это выглядит как оптимизация, где мы можем просто считать, что это та же самая функция.


Резюме с аналогией

С тем, что мы видели во втором случае, это выглядит почти так, как будто есть оптимизация, так как оба цикла имеют одинаковую точную сигнатуру, но это не настоящая проблема. Проблема не в работе, которую выполняют f(a) , f(b) , f(c) и f(d) в обоих случаях, а в сравнении между ними, а в разнице в расстоянии суммирования должен путешествовать в обоих случаях, что дает вам разницу во времени исполнения.

Представьте, что For Loops - это Summations , выполняющие итерации, как Boss который отдает приказы двум людям A B и что их работа заключается в том, чтобы добывать C D соответственно, а также собирать с них какую-то посылку и возвращать ее. В аналогии здесь сами итерации цикла или суммирования и проверки условий на самом деле не представляют Boss . То, что на самом деле представляет Boss здесь не непосредственно из фактических математических алгоритмов, а из фактической концепции Scope и Code Block внутри подпрограммы или подпрограммы, метода, функции, единицы перевода и т.д. Первый алгоритм имеет 1 область действия, где 2-й алгоритм имеет 2 последовательных области действия.

В первом случае при каждом вызове Boss идет к A и отдает приказ, а A уходит, чтобы получить пакет B's затем Boss идет к C и отдает приказы сделать то же самое и получать пакет от D на каждой итерации.

Во втором случае Boss работает напрямую с A чтобы пойти и получить пакет B's пока все пакеты не будут получены. Тогда Boss работает с C , чтобы сделать то же самое для получения всех D's пакетов.

Поскольку мы работаем с 8-байтовым указателем и занимаемся распределением кучи, давайте рассмотрим эту проблему здесь. Допустим, Boss находится в 100 футах от A а A - в 500 футах от C Нам не нужно беспокоиться о том, как далеко Boss изначально от C из-за порядка выполнения. В обоих случаях Boss сначала путешествует из A затем в B Эта аналогия не говорит о том, что это расстояние является точным; это всего лишь сценарий использования теста, чтобы показать работу алгоритмов. Во многих случаях при распределении кучи и работе с кешем и файлами подкачки эти расстояния между адресами могут не сильно различаться или могут очень сильно зависеть от характера типов данных и размеров массива.


Тестовые случаи:

Первый случай: на первой итерации Boss должен сначала пройти 100 футов, чтобы отдать ордер A и A уходит и делает свое дело, но затем Boss должен пройти 500 футов к C чтобы дать ему ордер. Затем на следующей итерации и на каждой другой итерации после того, как Boss должен пройти назад и вперед 500 футов между ними.

Второй случай: The 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*100); distTraveledOfFirst = 600 + 9999900; distTraveledOfFirst = 10000500; // Distance Traveled On First Algorithm = 10,000,500ft distTraveledOfSecond = 100 + 500 = 600; // Distance Traveled On Second Algorithm = 600ft; 

Сравнение произвольных значений

Мы легко видим, что 600 - это гораздо меньше, чем 10 миллионов. Теперь это не точно, потому что мы не знаем фактической разницы в расстоянии между тем, какой адрес ОЗУ или из какого Cache или файла подкачки каждый вызов на каждой итерации будет вызван многими другими невидимыми переменными, но это просто оценка ситуации, которую нужно знать, и попытка взглянуть на нее с наихудшего сценария.

Таким образом, по этим числам это будет выглядеть так, как будто Алгоритм Один должен быть на 99% медленнее, чем Алгоритм Два; однако, это только часть или ответственность The Boss's за алгоритмы, и она не учитывает фактических работников A , B , C и D а также то, что они должны делать на каждой итерации цикла. Таким образом, работа боссов составляет только около 15-40% всей выполняемой работы. Таким образом, основная часть работы, выполняемой рабочими, оказывает чуть большее влияние на поддержание соотношения разности скоростей примерно до 50-70%.


Наблюдение: - Различия между двумя алгоритмами

В этой ситуации это структура процесса выполняемой работы, и он показывает, что вариант 2 более эффективен как при частичной оптимизации, так и при наличии аналогичного объявления функции и определения, где только переменные различаются по имени., И мы также видим, что общее расстояние, пройденное в случае 1 , намного больше, чем в случае 2, и мы можем считать это расстояние пройденным нашим Фактором времени между двумя алгоритмами. Дело 1 требует гораздо больше работы, чем дело 2 . Это также было замечено в доказательстве ASM которое было показано между обоими случаями. Даже с учетом того, что уже было сказано об этих случаях, это также не учитывает того факта, что в случае 1 боссу придется ждать, пока оба A C вернутся, прежде чем он сможет снова вернуться к A на следующей итерации и это также не учитывает тот факт, что если A или B отнимает слишком много времени, то и Boss и другие работники также ожидают простоя. В случае 2 только один бездействует - Boss пока рабочий не вернется. Так что даже это влияет на алгоритм.



Измененные вопросы ОП

РЕДАКТИРОВАТЬ: Вопрос оказался неактуальным, так как поведение сильно зависит от размеров массивов (n) и кэш-памяти ЦП. Так что, если есть дальнейший интерес, я перефразирую вопрос:

Не могли бы вы дать некоторое подробное представление о деталях, которые приводят к разным поведениям кэша, как показано пятью областями на следующем графике?

Также было бы интересно указать на различия между архитектурами ЦП и кэш-памяти, предоставляя подобный график для этих ЦП.


Относительно этих вопросов

Без сомнения, я продемонстрировал, что существует проблема, лежащая в основе еще до того, как аппаратное и программное обеспечение подключается. Теперь что касается управления памятью и кэшированием вместе с файлами подкачки и т.д., Которые все работают вместе в интегрированном наборе систем между: The Architecture {Аппаратное обеспечение, Прошивка, некоторые встроенные драйверы, Ядра и Наборы инструкций ASM}, The OS {Файл и системы управления памятью, драйверы и реестр}, The Compiler {единицы перевода и оптимизации исходного кода} и даже сам Source Code с его набором (-ами) отличительных алгоритмов; мы уже можем видеть, что в первом алгоритме есть узкое место, прежде чем мы даже применим его к любой машине с любой произвольной Architecture , OS и Programmable > по сравнению со вторым алгоритмом. Таким образом, уже существовала проблема, прежде чем задействовать внутренние особенности современного компьютера.


Конечные результаты

Тем не мение; Нельзя сказать, что эти новые вопросы не важны, потому что они сами по себе и играют роль в конце концов. Они действительно влияют на процедуры и общую эффективность, и это видно из различных графиков и оценок от многих, кто дал свой ответ и/или комментарий (ы). Если вы обратите внимание на аналогию с Boss и двумя работниками A B которые должны были пойти и получить пакеты из C D соответственно, и с учетом математических обозначений этих двух алгоритмов, вы можете увидеть, что даже без участия Компьютерный Case 2 примерно на 60% быстрее, чем Case 1 и когда вы смотрите на графики и диаграммы после того, как эти алгоритмы были применены к исходному коду, скомпилированы и оптимизированы и выполнены через ОС для выполнения операций с данным оборудованием, вы даже немного видите большее ухудшение различий в этих алгоритмах.

Теперь, если набор "данных" довольно мал, на первый взгляд может показаться, что разница не такая уж и плохая, но поскольку 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 C для каждой итерации после первой итерации, в то время как Алгоритм 2 Boss должен был пройти к A один раз, а затем после того, как это было сделано с A он должен был проехать максимальное расстояние только один раз при переходе от A к C

Поэтому попытка заставить Boss сосредоточиться на двух одинаковых вещах одновременно и жонглировать ими взад и вперед вместо того, чтобы сосредоточиться на похожих последовательных задачах, к концу дня рассердит его, потому что ему пришлось путешествовать и работать вдвое больше., Поэтому не теряйте масштаб ситуации, позволяя вашему боссу попасть в интерполированное узкое место, потому что супруга и дети босса этого не оценят.

5
ответ дан Francis Cugler 30 янв. '17 в 17:00 2017-01-30 17:00

Это может быть старый C++ и оптимизации. На моем компьютере я получил почти такую же скорость:

Один цикл: 1,577 мс

Два цикла: 1,507 мс

Я использую Visual Studio 2015 на процессоре E5-1620 с частотой 3,5 ГГц и 16 ГБ ОЗУ.

1
ответ дан mathengineer 11 июля '18 в 10:00 2018-07-11 10:00

Другие вопросы по меткам или Задайте вопрос