Misalkan a1
, b1
, c1
, dan d1
arahkan ke memori tumpukan dan kode numerik saya memiliki loop inti berikut.
const int n = 100000;
for (int j = 0; j < n; j++) {
a1[j] += b1[j];
c1[j] += d1[j];
}
Loop ini dijalankan 10.000 kali melalui for
loop luar lainnya . Untuk mempercepatnya, saya mengubah kode menjadi:
for (int j = 0; j < n; j++) {
a1[j] += b1[j];
}
for (int j = 0; j < n; j++) {
c1[j] += d1[j];
}
Dikompilasi pada MS Visual C ++ 10.0 dengan optimasi penuh dan SSE2 diaktifkan untuk 32-bit pada Intel Core 2 Duo (x64), contoh pertama membutuhkan 5,5 detik dan contoh loop ganda hanya membutuhkan 1,9 detik. Pertanyaan saya adalah: (Silakan merujuk ke pertanyaan saya yang ditulis ulang di bagian bawah)
PS: Saya tidak yakin kalau ini membantu:
Pembongkaran untuk loop pertama pada dasarnya terlihat seperti ini (blok ini diulang sekitar lima kali dalam program penuh):
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]
Setiap loop dari contoh loop ganda menghasilkan kode ini (blok berikut diulang sekitar tiga kali):
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
Pertanyaan itu ternyata tidak ada relevansinya, karena perilakunya sangat tergantung pada ukuran array (n) dan cache CPU. Jadi jika ada minat lebih lanjut, saya ulangi pertanyaannya:
Bisakah Anda memberikan beberapa wawasan yang mendalam tentang detail yang mengarah pada perilaku cache yang berbeda seperti yang diilustrasikan oleh lima wilayah pada grafik berikut?
Mungkin juga menarik untuk menunjukkan perbedaan antara arsitektur CPU / cache, dengan menyediakan grafik yang sama untuk CPU ini.
PPS: Ini kode lengkapnya. Ini menggunakan TBB Tick_Count
untuk pengaturan waktu resolusi yang lebih tinggi, yang dapat dinonaktifkan dengan tidak mendefinisikan TBB_TIMING
Makro:
#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;
}
}
(Ini menunjukkan FLOP / s untuk nilai yang berbeda dari n
.)
sumber
restrict
kata kunci untuk situasi seperti itu. Saya tidak tahu apakah MSVC memiliki sesuatu yang serupa. Tentu saja, jika ini masalahnya maka kode SSE tidak akan benar.d1[j]
mungkin dengan aliasa1[j]
, sehingga kompiler dapat menarik diri dari melakukan beberapa optimasi memori. Sementara itu tidak terjadi jika Anda memisahkan tulisan ke memori dalam dua loop.Jawaban:
Setelah analisis lebih lanjut tentang ini, saya percaya ini (setidaknya sebagian) disebabkan oleh penyelarasan data dari empat-pointer. Ini akan menyebabkan beberapa level bank cache / konflik jalan.
Jika saya telah menebak dengan benar tentang bagaimana Anda mengalokasikan array Anda, mereka cenderung disejajarkan dengan baris halaman .
Ini berarti bahwa semua akses Anda di setiap loop akan jatuh pada cara cache yang sama. Namun, prosesor Intel memiliki 8-way L1 cache associativity untuk sementara waktu. Namun dalam kenyataannya, kinerjanya tidak sepenuhnya seragam. Mengakses 4-arah masih lebih lambat daripada mengatakan 2-arah.
EDIT: Itu sebenarnya terlihat seperti Anda mengalokasikan semua array secara terpisah. Biasanya ketika alokasi besar seperti itu diminta, pengalokasi akan meminta halaman baru dari OS. Oleh karena itu, ada kemungkinan besar bahwa alokasi besar akan muncul pada offset yang sama dari batas halaman.
Inilah kode tes:
Hasil Benchmark:
EDIT: Hasil dari mesin arsitektur Core 2 yang sebenarnya :
2 x Intel Xeon X5482 Harpertown @ 3.2 GHz:
Pengamatan:
6,206 detik dengan satu loop dan 2,116 detik dengan dua loop. Ini mereproduksi hasil OP persis.
Dalam dua tes pertama, array dialokasikan secara terpisah. Anda akan melihat bahwa mereka semua memiliki perataan yang relatif relatif terhadap halaman.
Dalam dua tes kedua, array dikemas bersama untuk memecah keselarasan itu. Di sini Anda akan melihat kedua loop lebih cepat. Selain itu, loop (ganda) kedua sekarang lebih lambat seperti yang biasanya Anda harapkan.
Seperti @Stephen Cannon tunjukkan dalam komentar, ada kemungkinan besar bahwa perataan ini menyebabkan alias palsu di unit load / store atau cache. Saya mencari-cari di Google untuk ini dan menemukan bahwa Intel sebenarnya memiliki penghitung perangkat keras untuk warung aliasing alamat parsial :
http://software.intel.com/sites/products/documentation/doclib/stdxe/2013/~amplifierxe/pmw_dp/events/partial_address_alias.html
5 Wilayah - Penjelasan
Wilayah 1:
Yang ini mudah. Dataset sangat kecil sehingga kinerjanya didominasi oleh overhead seperti looping dan branching.
Wilayah 2:
Di sini, ketika ukuran data meningkat, jumlah overhead relatif turun dan kinerja "jenuh". Di sini dua loop lebih lambat karena memiliki loop dua kali lebih banyak dan overhead bercabang.Saya tidak yakin persis apa yang terjadi di sini ... Keselarasan masih dapat berpengaruh sebagai Agner Fog menyebutkan konflik bank cache . (Tautan itu tentang Sandy Bridge, tetapi idenya harus tetap berlaku untuk Core 2.)
Wilayah 3:
Pada titik ini, data tidak lagi sesuai dengan cache L1. Jadi kinerjanya dibatasi oleh L1 <-> L2 cache bandwidth.
Wilayah 4:
Penurunan kinerja dalam satu-loop adalah apa yang kami amati. Dan seperti yang disebutkan, ini adalah karena penyejajaran yang (kemungkinan besar) menyebabkan warung aliasing palsu di unit pemuatan / penyimpanan prosesor.
Namun, agar kesalahan alias terjadi, harus ada langkah yang cukup besar antara set data. Inilah sebabnya mengapa Anda tidak melihat ini di wilayah 3.
Wilayah 5:
Pada titik ini, tidak ada yang cocok dengan cache. Jadi Anda terikat oleh bandwidth memori.
sumber
OK, jawaban yang benar pasti ada hubungannya dengan cache CPU. Tetapi untuk menggunakan argumen cache bisa sangat sulit, terutama tanpa data.
Ada banyak jawaban, yang mengarah ke banyak diskusi, tetapi mari kita hadapi: masalah cache bisa sangat kompleks dan tidak satu dimensi. Mereka sangat bergantung pada ukuran data, jadi pertanyaan saya tidak adil: Ternyata pada titik yang sangat menarik dalam grafik cache.
Jawaban @ Mysticial meyakinkan banyak orang (termasuk saya), mungkin karena itu satu-satunya yang tampaknya mengandalkan fakta, tetapi itu hanya satu "titik data" dari kebenaran.
Itu sebabnya saya menggabungkan tesnya (menggunakan alokasi kontinu vs terpisah) dan saran @James 'Answers.
Grafik di bawah ini menunjukkan, bahwa sebagian besar jawaban dan terutama sebagian besar komentar terhadap pertanyaan dan jawaban dapat dianggap sepenuhnya salah atau benar tergantung pada skenario dan parameter yang digunakan.
Perhatikan bahwa pertanyaan awal saya adalah n = 100.000 . Poin ini (secara tidak sengaja) menunjukkan perilaku khusus:
Ini memiliki perbedaan terbesar antara versi satu dan dua loop (hampir faktor tiga)
Ini adalah satu-satunya titik, di mana satu loop (yaitu dengan alokasi kontinu) mengalahkan versi dua loop. (Ini memungkinkan jawaban Mysticial, sama sekali.)
Hasilnya menggunakan data yang diinisialisasi:
Hasilnya menggunakan data yang tidak diinisialisasi (inilah yang diuji Mysticial):
Dan ini yang sulit dijelaskan: Data diinisialisasi, yang dialokasikan satu kali dan digunakan kembali untuk setiap uji kasus berikut dengan ukuran vektor yang berbeda:
Usul
Setiap pertanyaan terkait kinerja tingkat rendah pada Stack Overflow harus diminta untuk memberikan informasi MFLOPS untuk seluruh jajaran ukuran data yang relevan dengan cache! Buang-buang waktu semua orang untuk memikirkan jawaban dan terutama mendiskusikannya dengan orang lain tanpa informasi ini.
sumber
n
dan itu menunjukkan kesenjangan kinerja yang sama untukn = 80000, n = 100000, n = 200000
, dll ...VirtualAlloc
panggilan memblokir hingga cukup nol untuk memenuhi permintaan. Sebaliknya, Linux hanya memetakan halaman nol sebagai copy-on-write sebanyak yang diperlukan, dan pada saat menulis, ia menyalin nol baru ke halaman baru sebelum menulis dalam data baru. Either way, dari perspektif proses mode pengguna, halaman-halamannya adalah nol, tetapi penggunaan pertama dari memori yang tidak diinisialisasi biasanya akan lebih mahal di Linux daripada di Windows.Loop kedua melibatkan aktivitas cache yang jauh lebih sedikit, sehingga prosesor lebih mudah untuk mengikuti tuntutan memori.
sumber
a[i]
,b[i]
,c[i]
dand[i]
Dalam varian kedua, perlu hanya dua. Ini membuatnya jauh lebih layak untuk mengisi ulang garis-garis tersebut sambil menambahkan.x += y
), ada dua membaca dan satu menulis. Ini berlaku untuk kedua varian. Cache <-> Persyaratan bandwidth CPU adalah sama. Selama tidak ada konflik, cache <-> Persyaratan bandwidth RAM juga sama ..Bayangkan Anda sedang mengerjakan mesin di mana
n
nilai yang tepat hanya memungkinkan untuk menahan dua array Anda dalam memori pada satu waktu, tetapi total memori yang tersedia, melalui cache disk, masih cukup untuk menampung keempatnya.Dengan asumsi kebijakan caching LIFO sederhana, kode ini:
pertama-tama akan menyebabkan
a
danb
dimuat ke dalam RAM dan kemudian dikerjakan sepenuhnya dalam RAM. Ketika loop kedua dimulai,c
dand
kemudian akan dimuat dari disk ke dalam RAM dan dioperasikan.loop lainnya
akan mengeluarkan dua array dan halaman di dua lainnya setiap kali di loop . Ini jelas akan banyak lebih lambat.
Anda mungkin tidak melihat caching disk dalam tes Anda, tetapi Anda mungkin melihat efek samping dari beberapa bentuk caching lainnya.
Tampaknya ada sedikit kebingungan / kesalahpahaman di sini jadi saya akan mencoba menguraikan sedikit menggunakan contoh.
Katakan
n = 2
dan kami bekerja dengan byte. Dalam skenario saya demikian kita miliki hanya 4 byte RAM dan sisa memori kami secara signifikan lebih lambat (katakanlah akses 100 kali lebih lama).Dengan asumsi kebijakan caching yang cukup bodoh jika byte tidak ada dalam cache, letakkan di sana dan dapatkan byte berikut juga saat kita berada di dalamnya, Anda akan mendapatkan skenario seperti ini:
Dengan
cache
a[0]
dana[1]
kemudianb[0]
danb[1]
dan diatura[0] = a[0] + b[0]
dalam cache - sekarang ada empat byte dalam cache,a[0], a[1]
danb[0], b[1]
. Biaya = 100 + 100.a[1] = a[1] + b[1]
dalam cache. Biaya = 1 + 1.c
dand
.Total biaya =
(100 + 100 + 1 + 1) * 2 = 404
Dengan
cache
a[0]
dana[1]
kemudianb[0]
danb[1]
dan diatura[0] = a[0] + b[0]
dalam cache - sekarang ada empat byte dalam cache,a[0], a[1]
danb[0], b[1]
. Biaya = 100 + 100.a[0], a[1], b[0], b[1]
dari cache dan cachec[0]
danc[1]
kemudiand[0]
dand[1]
dan setc[0] = c[0] + d[0]
dalam cache. Biaya = 100 + 100.(100 + 100 + 100 + 100) * 2 = 800
Ini adalah skenario thrash cache klasik.
sumber
a1
danb1
untuk tugas pertama, bukan hanya halaman pertama dari masing-masing? (Apakah Anda mengasumsikan halaman 5-byte, jadi satu halaman adalah setengah dari RAM Anda? Itu bukan hanya penskalaan, itu sama sekali tidak seperti prosesor yang sebenarnya.)Ini bukan karena kode yang berbeda, tetapi karena caching: RAM lebih lambat dari register CPU dan memori cache ada di dalam CPU untuk menghindari penulisan RAM setiap kali variabel berubah. Tetapi cache tidak sebesar RAM, karenanya hanya memetakan sebagian saja.
Kode pertama memodifikasi alamat memori jauh secara bergantian di setiap loop, sehingga membutuhkan terus menerus untuk membatalkan cache.
Kode kedua tidak berganti: hanya mengalir di alamat yang berdekatan dua kali. Ini membuat semua pekerjaan harus diselesaikan dalam cache, membatalkannya hanya setelah loop kedua dimulai.
sumber
Saya tidak bisa meniru hasil yang dibahas di sini.
Saya tidak tahu apakah kode tolok ukur yang salah harus disalahkan, atau apa, tetapi kedua metode tersebut berada dalam jarak 10% satu sama lain pada mesin saya menggunakan kode berikut, dan satu putaran biasanya hanya sedikit lebih cepat dari dua - seperti yang Anda inginkan mengharapkan.
Ukuran array berkisar dari 2 ^ 16 hingga 2 ^ 24, menggunakan delapan loop. Saya berhati-hati untuk menginisialisasi array sumber sehingga
+=
tugas tidak meminta FPU untuk menambahkan memori sampah yang ditafsirkan sebagai ganda.Saya bermain-main dengan berbagai skema, seperti menempatkan penugasan
b[j]
,d[j]
keInitToZero[j]
dalam loop, dan juga dengan menggunakan+= b[j] = 1
dan+= d[j] = 1
, dan saya mendapatkan hasil yang cukup konsisten.Seperti yang Anda harapkan, menginisialisasi
b
dand
di dalam loop menggunakanInitToZero[j]
memberi pendekatan gabungan keuntungan, karena mereka dilakukan back-to-back sebelum penugasan kea
danc
, tetapi masih dalam 10%. Sosok pergi.Perangkat kerasnya adalah Dell XPS 8500 dengan generasi 3 Core i7 @ 3.4 GHz dan memori 8 GB. Untuk 2 ^ 16 hingga 2 ^ 24, menggunakan delapan loop, waktu kumulatif adalah masing-masing 44,987 dan 40,965. Visual C ++ 2010, sepenuhnya dioptimalkan.
PS: Saya mengubah loop untuk menghitung mundur ke nol, dan metode gabungan sedikit lebih cepat. Menggaruk kepalaku. Perhatikan ukuran array dan jumlah loop baru.
Saya tidak yakin mengapa diputuskan bahwa MFLOPS adalah metrik yang relevan. Saya pikir idenya adalah untuk fokus pada akses memori, jadi saya mencoba untuk meminimalkan jumlah waktu komputasi floating point. Saya pergi di
+=
, tetapi saya tidak yakin mengapa.Tugas lurus tanpa perhitungan akan menjadi tes yang lebih bersih dari waktu akses memori dan akan membuat tes yang seragam terlepas dari jumlah loop. Mungkin saya melewatkan sesuatu dalam percakapan itu, tetapi perlu dipikirkan dua kali. Jika plus ditinggalkan dari tugas, waktu kumulatif hampir identik pada 31 detik masing-masing.
sumber
Itu karena CPU tidak memiliki cache yang terlalu banyak (di mana ia harus menunggu data array datang dari chip RAM). Akan menarik bagi Anda untuk menyesuaikan ukuran array secara terus-menerus sehingga Anda melebihi ukuran cache level 1 (L1), dan kemudian cache level 2 (L2), dari CPU Anda dan plot waktu yang diperlukan untuk kode Anda untuk mengeksekusi terhadap ukuran array. Grafik tidak boleh berupa garis lurus seperti yang Anda harapkan.
sumber
Loop pertama bergantian menulis di setiap variabel. Yang kedua dan ketiga hanya membuat lompatan kecil dari ukuran elemen.
Cobalah menulis dua garis sejajar 20 salib dengan pena dan kertas yang dipisahkan 20 cm. Cobalah sekali menyelesaikan satu dan kemudian baris lainnya dan coba lain waktu dengan menuliskan tanda silang di setiap baris secara bergantian.
sumber
Pertanyaan Asli
Kesimpulan:
Kasus 1 adalah masalah interpolasi klasik yang kebetulan tidak efisien. Saya juga berpikir bahwa ini adalah salah satu alasan utama mengapa banyak arsitektur dan pengembang mesin akhirnya membangun dan merancang sistem multi-inti dengan kemampuan untuk melakukan aplikasi multi-berulir serta pemrograman paralel.
Melihatnya dari pendekatan semacam ini tanpa melibatkan bagaimana Perangkat Keras, OS, dan Kompiler bekerja bersama untuk melakukan alokasi tumpukan yang melibatkan bekerja dengan RAM, Cache, File Halaman, dll .; matematika yang merupakan dasar dari algoritma ini menunjukkan kepada kita yang mana dari dua ini adalah solusi yang lebih baik.
Kita dapat menggunakan analogi
Boss
makhlukSummation
yang akan mewakiliFor Loop
yang harus bepergian antara pekerjaA
&B
.Kita dapat dengan mudah melihat bahwa Kasus 2 setidaknya setengah lebih cepat jika tidak sedikit lebih dari Kasus 1 karena perbedaan jarak yang diperlukan untuk melakukan perjalanan dan waktu yang diambil antara pekerja. Matematika ini berbaris hampir secara virtual dan sempurna dengan BenchMark Times serta jumlah perbedaan dalam Instruksi Majelis.
Sekarang saya akan mulai menjelaskan bagaimana semua ini bekerja di bawah ini.
Menilai Masalahnya
Kode OP:
Dan
Pertimbangannya
Mempertimbangkan pertanyaan awal OP tentang 2 varian for for loop dan pertanyaannya yang diubah terhadap perilaku cache bersama dengan banyak jawaban luar biasa lainnya dan komentar yang berguna; Saya ingin mencoba dan melakukan sesuatu yang berbeda di sini dengan mengambil pendekatan berbeda tentang situasi dan masalah ini.
Pendekatan
Mempertimbangkan dua loop dan semua diskusi tentang cache dan pengarsipan halaman saya ingin mengambil pendekatan lain untuk melihat ini dari perspektif yang berbeda. Salah satu yang tidak melibatkan cache dan file halaman atau eksekusi untuk mengalokasikan memori, pada kenyataannya, pendekatan ini bahkan tidak menyangkut perangkat keras atau perangkat lunak yang sebenarnya sama sekali.
Perspektif
Setelah melihat kode untuk sementara waktu menjadi sangat jelas apa masalahnya dan apa yang menghasilkannya. Mari kita memecah ini menjadi masalah algoritmik dan melihatnya dari perspektif menggunakan notasi matematika kemudian menerapkan analogi untuk masalah matematika dan juga algoritma.
Apa Yang Kita Ketahui
Kita tahu bahwa loop ini akan berjalan 100.000 kali. Kita juga tahu bahwa
a1
,b1
,c1
&d1
adalah pointer pada arsitektur 64-bit. Dalam C ++ pada mesin 32-bit, semua pointer berukuran 4 byte dan pada mesin 64-bit, semuanya berukuran 8 byte karena pointer memiliki panjang yang tetap.Kami tahu bahwa kami memiliki 32 byte untuk dialokasikan dalam kedua kasus. Satu-satunya perbedaan adalah kita mengalokasikan 32 byte atau 2 set 2-8bytes pada setiap iterasi di mana kasus ke-2 kita mengalokasikan 16 byte untuk setiap iterasi untuk kedua loop independen.
Kedua loop masih sama 32 byte dalam alokasi total. Dengan informasi ini, mari kita lanjutkan dan tunjukkan matematika umum, algoritma, dan analogi dari konsep-konsep ini.
Kami tahu berapa kali set atau kelompok operasi yang sama yang harus dilakukan dalam kedua kasus. Kami tahu jumlah memori yang perlu dialokasikan dalam kedua kasus. Kita dapat menilai bahwa beban kerja keseluruhan dari alokasi antara kedua kasus akan kira-kira sama.
Yang Tidak Kami Ketahui
Kami tidak tahu berapa lama untuk setiap kasus kecuali jika kami menetapkan penghitung dan menjalankan tes benchmark. Namun, tolok ukur sudah termasuk dari pertanyaan awal dan dari beberapa jawaban dan komentar juga; dan kita dapat melihat perbedaan yang signifikan antara keduanya dan ini adalah alasan keseluruhan untuk proposal ini untuk masalah ini.
Mari selidiki
Sudah jelas bahwa banyak yang telah melakukan ini dengan melihat alokasi heap, tes benchmark, melihat RAM, Cache, dan File Halaman. Melihat titik data spesifik dan indeks iterasi spesifik juga dimasukkan dan berbagai percakapan tentang masalah khusus ini membuat banyak orang mulai mempertanyakan hal-hal terkait lainnya tentang hal itu. Bagaimana kita mulai melihat masalah ini dengan menggunakan algoritma matematika dan menerapkan analogi untuk itu? Kami memulai dengan membuat beberapa pernyataan! Lalu kami membangun algoritma kami dari sana.
Pernyataan Kami:
F1()
,F2()
,f(a)
,f(b)
,f(c)
danf(d)
.Algoritma:
Kasus 1: - Hanya satu penjumlahan tetapi dua panggilan fungsi independen.
Kasus 2: - Dua penjumlahan tetapi masing-masing memiliki panggilan fungsi tersendiri.
Jika Anda perhatikan
F2()
hanya ada diSum
dariCase1
manaF1()
terkandungSum
dariCase1
dan di keduanyaSum1
danSum2
dariCase2
. Ini akan menjadi bukti nanti ketika kita mulai menyimpulkan bahwa ada optimasi yang terjadi dalam algoritma kedua.Iterasi melalui
Sum
panggilan kasus pertamaf(a)
yang akan menambah dirinya sendirif(b)
lalu panggilanf(c)
yang akan melakukan hal yang sama tetapi menambahf(d)
sendiri untuk setiap100000
iterasi. Dalam kasus kedua, kita memilikiSum1
danSum2
keduanya bertindak sama seolah-olah mereka memiliki fungsi yang sama dipanggil dua kali berturut-turut.Dalam hal ini kita dapat memperlakukan
Sum1
danSum2
sama seperti biasa diSum
manaSum
dalam kasus ini terlihat seperti ini:Sum n=1 : [1,100000] { f(a) = f(a) + f(b); }
dan sekarang ini terlihat seperti optimasi di mana kita dapat menganggapnya sebagai fungsi yang sama.Ringkasan dengan Analogi
Dengan apa yang telah kita lihat dalam kasus kedua hampir tampak seolah-olah ada optimasi karena keduanya untuk loop memiliki tanda tangan yang sama persis, tetapi ini bukan masalah sebenarnya. Masalahnya bukan pekerjaan yang sedang dilakukan oleh
f(a)
,f(b)
,f(c)
, danf(d)
. Dalam kedua kasus dan perbandingan antara keduanya, itu adalah perbedaan dalam jarak yang dijumlahkan oleh Penjumlahan dalam setiap kasus yang memberi Anda perbedaan dalam waktu eksekusi.Pikirkan
For Loops
sebagaiSummations
yang melakukan iterasi sebagaiBoss
yang memberi perintah kepada dua orangA
&B
dan bahwa pekerjaan mereka adalah dagingC
&D
masing-masing dan untuk mengambil beberapa paket dari mereka dan mengembalikannya. Dalam analogi ini, perulangan dan penjumlahan for loop atau penjumlahan kondisi sendiri sebenarnya tidak mewakiliBoss
. Apa yang sebenarnya mewakiliBoss
bukan dari algoritma matematika aktual secara langsung tetapi dari konsep aktualScope
danCode Block
dalam rutin atau subrutin, metode, fungsi, unit terjemahan, dll. Algoritma pertama memiliki 1 ruang lingkup di mana algoritma 2 memiliki 2 cakupan berturut-turut.Dalam kasus pertama pada setiap slip panggilan,
Boss
pergi keA
dan memberikan pesanan danA
pergi untuk mengambilB's
paket kemudianBoss
pergi keC
dan memberikan perintah untuk melakukan hal yang sama dan menerima paket dariD
pada setiap iterasi.Dalam kasus kedua,
Boss
pekerjaan langsung denganA
pergi dan mengambilB's
paket sampai semua paket diterima. KemudianBoss
bekerja denganC
melakukan hal yang sama untuk mendapatkan semuaD's
paket.Karena kita bekerja dengan pointer 8-byte dan berurusan dengan alokasi heap mari kita pertimbangkan masalah berikut. Katakanlah
Boss
100 kaki dariA
danA
500 kaki dariC
. Kita tidak perlu khawatir tentang seberapa jauh dariBoss
awalnyaC
karena urutan eksekusi. Dalam kedua kasus,Boss
awalnya perjalanan dariA
pertama lalu keB
. Analogi ini bukan untuk mengatakan bahwa jarak ini tepat; itu hanya skenario kasus uji yang berguna untuk menunjukkan cara kerja algoritma.Dalam banyak kasus ketika melakukan alokasi tumpukan dan bekerja dengan cache dan file halaman, jarak antara lokasi alamat ini mungkin tidak banyak berbeda atau mereka dapat bervariasi secara signifikan tergantung pada sifat tipe data dan ukuran array.
Kasus Uji:
Kasus Pertama: Pada iterasi pertama
Boss
harus awalnya berjalan 100 kaki untuk memberikan slip pesanan keA
danA
pergi dan melakukan hal-nya, tetapi kemudianBoss
harus melakukan perjalanan 500 kaki untukC
memberinya slip pesanannya. Kemudian pada iterasi berikutnya dan setiap iterasi lainnya setelahBoss
harus bolak-balik 500 kaki antara keduanya.Kasus Kedua: The
Boss
harus melakukan perjalanan 100 kaki pada iterasi pertamaA
, tetapi setelah itu, dia sudah ada di sana dan hanya menunggu untukA
kembali sampai semua slip terisi. KemudianBoss
harus menempuh jarak 500 kaki pada iterasi pertamaC
karenaC
berjarak 500 kaki dariA
. Karena iniBoss( Summation, For Loop )
dipanggil tepat setelah bekerja denganA
dia maka hanya menunggu di sana seperti yang dia lakukan denganA
sampai semuaC's
slip pesanan selesai.Perbedaan Jarak Yang Dijalani
Perbandingan Nilai Sewenang-wenang
Kita dapat dengan mudah melihat bahwa 600 jauh lebih sedikit dari 10 juta. Sekarang, ini tidak tepat, karena kita tidak tahu perbedaan sebenarnya dalam jarak antara alamat RAM atau dari mana Cache atau File Halaman setiap panggilan pada setiap iterasi akan disebabkan oleh banyak variabel tak terlihat lainnya. Ini hanya penilaian situasi yang harus diperhatikan dan dilihat dari skenario terburuk.
Dari angka-angka ini hampir akan tampak seolah-olah Algoritma One harus lebih
99%
lambat dari Algoritma Dua; Namun, ini hanyaBoss's
sebagian atau tanggung jawab algoritma dan tidak memperhitungkan pekerja yang sebenarnyaA
,B
,C
, &D
dan apa yang harus mereka lakukan pada setiap iterasi dari Loop. Jadi pekerjaan bos hanya menyumbang sekitar 15 - 40% dari total pekerjaan yang dilakukan. Sebagian besar pekerjaan yang dilakukan melalui pekerja memiliki dampak yang sedikit lebih besar untuk menjaga rasio perbedaan kecepatan menjadi sekitar 50-70%Pengamatan: - Perbedaan antara kedua algoritma
Dalam situasi ini, itu adalah struktur dari proses pekerjaan yang dilakukan. Ini menunjukkan bahwa Kasus 2 lebih efisien dari optimasi parsial memiliki pernyataan fungsi dan definisi yang sama di mana hanya variabel yang berbeda berdasarkan nama dan jarak yang ditempuh.
Kami juga melihat bahwa jarak total yang ditempuh dalam Kasus 1 jauh lebih jauh daripada dalam Kasus 2 dan kami dapat mempertimbangkan jarak yang ditempuh Faktor Waktu kami antara kedua algoritma. Kasus 1 memiliki banyak pekerjaan yang harus dilakukan daripada Kasus 2 .
Ini dapat diamati dari bukti
ASM
instruksi yang ditunjukkan dalam kedua kasus. Bersamaan dengan apa yang telah dinyatakan tentang kasus-kasus ini, ini tidak memperhitungkan fakta bahwa dalam Kasus 1 bos harus menunggu keduanyaA
&C
untuk kembali sebelum dia dapat kembaliA
lagi untuk setiap iterasi. Ini juga tidak memperhitungkan fakta bahwa jikaA
atauB
membutuhkan waktu yang sangat lama makaBoss
pekerja dan pekerja lainnya menganggur menunggu untuk dieksekusi.Dalam Kasus 2 satu-satunya yang menganggur adalah
Boss
sampai pekerja kembali. Jadi, ini pun berdampak pada algoritma.Pertanyaan yang Diubah OPs
Mengenai Pertanyaan Ini
Seperti yang telah saya tunjukkan tanpa keraguan, ada masalah mendasar bahkan sebelum Hardware dan Software terlibat.
Sekarang untuk pengelolaan memori dan caching bersama dengan file halaman, dll. Yang semuanya bekerja bersama dalam satu set sistem terintegrasi antara yang berikut:
The Architecture
{Perangkat Keras, Firmware, beberapa Driver Tertanam, Kernel dan Set Instruksi ASM}.The OS
{Sistem Manajemen File dan Memori, Driver dan Registry}.The Compiler
{Unit Terjemahan dan Optimalisasi Kode Sumber}.Source Code
itu sendiri dengan set (s) algoritma yang berbeda.Kita sudah bisa melihat bahwa ada hambatan yang terjadi dalam algoritma pertama sebelum kita bahkan menerapkannya pada setiap mesin dengan sewenang-wenang setiap
Architecture
,OS
danProgrammable Language
dibandingkan dengan algoritma kedua. Sudah ada masalah sebelum melibatkan intrinsik komputer modern.Hasil Akhir
Namun; bukan untuk mengatakan bahwa pertanyaan-pertanyaan baru ini tidak penting karena mereka sendiri dan mereka memang memainkan peran. Mereka berdampak pada prosedur dan kinerja keseluruhan dan itu terbukti dengan berbagai grafik dan penilaian dari banyak orang yang telah memberikan jawaban dan komentar mereka.
Jika Anda memperhatikan analogi dari
Boss
dan dua pekerjaA
&B
yang harus pergi dan mengambil paket dariC
&D
masing-masing dan mempertimbangkan notasi matematika dari dua algoritma yang dimaksud; Anda dapat melihat tanpa keterlibatan perangkat keras dan perangkat lunak komputerCase 2
lebih60%
cepat dariCase 1
.Ketika Anda melihat grafik dan bagan setelah algoritma ini telah diterapkan pada beberapa kode sumber, dikompilasi, dioptimalkan, dan dieksekusi melalui OS untuk melakukan operasi mereka pada perangkat keras yang diberikan, Anda bahkan dapat melihat sedikit lebih banyak degradasi antara perbedaan. dalam algoritma ini.
Jika
Data
himpunannya cukup kecil, mungkin pada awalnya tidak terlalu buruk. Namun, karenaCase 1
ini60 - 70%
lebih lambat daripada yangCase 2
bisa kita lihat pada pertumbuhan fungsi ini dalam hal perbedaan dalam eksekusi waktu:Perkiraan ini adalah perbedaan rata-rata antara kedua loop ini baik secara algoritmik dan operasi mesin yang melibatkan optimasi perangkat lunak dan instruksi mesin.
Ketika kumpulan data tumbuh secara linear, demikian juga perbedaan waktu antara keduanya. Algoritma 1 memiliki lebih banyak pengambilan daripada algoritma 2 yang terbukti ketika
Boss
harus melakukan perjalanan bolak-balik jarak maksimum antaraA
&C
untuk setiap iterasi setelah iterasi pertama sedangkan Algoritma 2Boss
harus melakukan perjalanan keA
sekali dan kemudian setelah dilakukan denganA
dia harus melakukan perjalanan jarak maksimum hanya satu kali ketika pergi dariA
keC
.Mencoba
Boss
memfokuskan pada melakukan dua hal yang sama sekaligus dan menyulapnya bolak-balik alih-alih berfokus pada tugas yang sama secara berurutan akan membuatnya marah pada akhir hari karena ia harus bepergian dan bekerja dua kali lebih banyak. Karena itu, jangan kehilangan ruang lingkup situasi dengan membiarkan atasan Anda mengalami hambatan yang diinterpolasi karena pasangan dan anak-anak bos tidak akan menghargainya.Amandemen: Prinsip Desain Rekayasa Perangkat Lunak
- Perbedaan antara
Local Stack
danHeap Allocated
perhitungan dalam iteratif untuk loop dan perbedaan antara penggunaannya, efisiensi, dan efektivitasnya -Algoritma matematika yang saya usulkan di atas terutama berlaku untuk loop yang melakukan operasi pada data yang dialokasikan pada heap.
Jadi ketika Anda bekerja dengan data yang perlu di tumpukan dan Anda melintasi mereka dalam loop, itu lebih efisien untuk menjaga setiap set data dan algoritma yang sesuai dalam satu loop sendiri. Anda akan mendapatkan optimasi yang lebih baik dibandingkan dengan mencoba memfaktorkan loop berturut-turut dengan menempatkan beberapa operasi set data yang berbeda yang ada di tumpukan menjadi satu loop.
Tidak apa-apa untuk melakukan ini dengan data yang ada di stack karena mereka sering di-cache, tetapi tidak untuk data yang harus memiliki alamat memorinya ditanyai setiap iterasi.
Di sinilah Rekayasa Perangkat Lunak dan Desain Arsitektur Perangkat Lunak berperan. Ini adalah kemampuan untuk mengetahui bagaimana mengatur data Anda, mengetahui kapan harus menyimpan data Anda, mengetahui kapan harus mengalokasikan data Anda di heap, mengetahui bagaimana merancang dan mengimplementasikan algoritma Anda, dan mengetahui kapan dan di mana memanggilnya.
Anda mungkin memiliki algoritma yang sama yang berkaitan dengan kumpulan data yang sama, tetapi Anda mungkin ingin satu desain implementasi untuk varian stack-nya dan yang lain untuk varian heap-dialokasikan hanya karena masalah di atas yang dilihat dari
O(n)
kompleksitas algoritma ketika bekerja dengan heap.Dari apa yang saya perhatikan selama bertahun-tahun banyak orang tidak mempertimbangkan fakta ini. Mereka akan cenderung merancang satu algoritma yang bekerja pada set data tertentu dan mereka akan menggunakannya terlepas dari set data yang di-cache secara lokal di stack atau jika dialokasikan pada heap.
Jika Anda menginginkan pengoptimalan yang benar, ya itu mungkin tampak seperti duplikasi kode, tetapi untuk menggeneralisasi akan lebih efisien untuk memiliki dua varian dari algoritma yang sama. Satu untuk operasi stack, dan lainnya untuk operasi heap yang dilakukan dalam loop berulang!
Berikut adalah contoh pseudo: Dua struct sederhana, satu algoritma.
Inilah yang saya maksudkan dengan memiliki implementasi terpisah untuk varian stack versus varian heap. Algoritme itu sendiri tidak terlalu penting, itu adalah struktur pengulangan yang akan Anda gunakan.
sumber
Mungkin C ++ lama dan optimisasi. Di komputer saya, saya memperoleh kecepatan yang hampir sama:
Satu loop: 1,577 ms
Dua loop: 1,507 ms
Saya menjalankan Visual Studio 2015 pada prosesor E5-1620 3.5 GHz dengan RAM 16 GB.
sumber