Orijinal Soru
Bir döngü neden iki döngüden daha yavaş?
Sonuç:
Dava 1 verimsiz olan klasik bir enterpolasyon problemidir. Bunun, birçok makine mimarisinin ve geliştiricisinin, paralel işlemenin yanı sıra çok iş parçacıklı uygulamalar yapma yeteneğine sahip çok çekirdekli sistemler oluşturup tasarlamasının önde gelen nedenlerinden biri olduğunu düşünüyorum.
Donanım, İşletim Sistemi ve Derleyici (ler) in RAM, Önbellek, Sayfa Dosyaları vb. İle çalışmayı içeren yığın tahsisleri yapmak için birlikte nasıl çalıştığını içermeden bu tür bir yaklaşımdan bakmak; bu algoritmaların temelini oluşturan matematik bize bu ikisinden hangisinin daha iyi bir çözüm olduğunu gösterir.
Biz bir benzetme kullanabilirsiniz Boss
bir varlık Summation
bir temsil edeceğini For Loop
işçiler arasında seyahat gerektiğini A
& B
.
Biz kolayca görebiliriz Durum 2 hızlı birden değil biraz sanki az yarısı olan Durumunda 1 nedeniyle seyahat ve işçiler arasında geçen zamandır ihtiyaç duyulan mesafe farkı. Bu matematik, neredeyse BenchMark Times'ın yanı sıra Montaj Talimatları'ndaki farklılıkların sayısı ile neredeyse sanal ve mükemmel bir şekilde sıralanır.
Şimdi tüm bunların nasıl çalıştığını aşağıda açıklamaya başlayacağım.
Sorunu Değerlendirme
OP'nin kodu:
const int n=100000;
for(int j=0;j<n;j++){
a1[j] += b1[j];
c1[j] += d1[j];
}
Ve
for(int j=0;j<n;j++){
a1[j] += b1[j];
}
for(int j=0;j<n;j++){
c1[j] += d1[j];
}
Düşünme
OP'nin döngülerin 2 varyantı hakkındaki orijinal sorusu ve önbelleklerin davranışına yönelik değiştirilmiş sorusu ve diğer birçok mükemmel cevap ve faydalı yorum göz önüne alındığında; Bu durum ve sorun hakkında farklı bir yaklaşım benimseyerek burada farklı bir şeyler yapmak istiyorum.
Yaklaşım
İki döngüyü ve önbellek ve sayfa dosyalama hakkındaki tüm tartışmaları göz önünde bulundurarak, buna farklı bir perspektiften bakmak için başka bir yaklaşım kullanmak istiyorum. Önbellek ve sayfa dosyalarını veya bellek ayırma işlemlerini içermeyen bir şey, aslında, bu yaklaşım gerçek donanım veya yazılımla bile ilgili değildir.
Bakış açısı
Bir süre koda baktıktan sonra sorunun ne olduğu ve neyin üretildiği oldukça belirgin hale geldi. Bunu algoritmik bir soruna ayıralım ve matematiksel gösterimleri kullanma perspektifinden bakalım, daha sonra matematik problemlerine ve algoritmalara bir benzetme uygulayalım.
Ne Biliyoruz
Bu döngünün 100.000 kez çalışacağını biliyoruz. Biz de biliyoruz a1
, b1
, c1
ved1
64-bit mimarisine göstericisidir. 32 bitlik bir makinedeki C ++ içinde, tüm işaretçiler 4 bayt ve 64 bitlik bir makinede, işaretçiler sabit uzunlukta olduğundan 8 bayt boyutundadır.
Her iki durumda da ayırmamız gereken 32 bayt olduğunu biliyoruz. Tek fark, her yinelemede 32 bayt veya 2 set 2-8bayt tahsis etmemizdir; burada 2. durum, her iki bağımsız döngü için her yineleme için 16 bayt tahsis ederiz.
Her iki döngü de toplam ayırmada 32 bayta eşittir. Bu bilgi ile şimdi devam edelim ve bu kavramların genel matematiğini, algoritmalarını ve benzetmesini gösterelim.
Her iki durumda da aynı grup veya işlem grubunun kaç kez yapılması gerektiğini biliyoruz. Her iki durumda da tahsis edilmesi gereken bellek miktarını biliyoruz. Her iki durum arasındaki tahsisatların toplam iş yükünün yaklaşık olarak aynı olacağını değerlendirebiliriz.
Bilmediklerimiz
Bir sayaç ayarlayıp bir kıyaslama testi yapmazsak, her dava için ne kadar süreceğini bilmiyoruz. Ancak, kriterler orijinal sorudan ve bazı cevap ve yorumlardan zaten dahil edildi; ve ikisi arasında önemli bir fark olduğunu görebiliriz ve bu sorun için bu teklifin tüm sebebi budur.
Araştıralım
Birçok kişinin bunu zaten yığın tahsislerine, kıyaslama testlerine, RAM, Önbellek ve Sayfa Dosyalarına bakarak zaten yapmış olduğu açıktır. Belirli veri noktalarına ve spesifik yineleme indekslerine bakıldığında da dahil edildi ve bu özel sorunla ilgili çeşitli görüşmeler birçok insanın ilgili diğer şeyleri sorgulamaya başladı. Matematiksel algoritmalar kullanarak ve buna benzetme yaparak bu soruna nasıl bakmaya başlarız? Birkaç iddia yaparak başlıyoruz! Sonra algoritmamızı oradan oluştururuz.
İddialarımız:
- Döngümüzün ve yinelemelerinin, döngülerde olduğu gibi 0 ile başlamak yerine 1'de başlayan ve 100000'de biten bir Summation olmasına izin vereceğiz, çünkü sadece ilgilendiğimiz için bellek adreslemesinin 0 indeksleme şeması hakkında endişelenmemize gerek yok. algoritmanın kendisi.
- Her iki durumda da, üzerinde çalışacağımız 4 fonksiyon ve her fonksiyon çağrısında 2 işlem yapılmış 2 fonksiyon çağrımız vardır. Biz Aşağıdaki gibi fonksiyonlara işlevleri ve çağrılar gibi bu kadar ayarlayacaktır:
F1()
, F2()
, f(a)
, f(b)
, f(c)
ve f(d)
.
Algoritmalar:
1. Durum: - Sadece bir toplam değil, iki bağımsız fonksiyon çağrısı.
Sum n=1 : [1,100000] = F1(), F2();
F1() = { f(a) = f(a) + f(b); }
F2() = { f(c) = f(c) + f(d); }
2. Durum: - İki özet ama her birinin kendi işlev çağrısı vardır.
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); }
Fark ise F2()
sadece var olan Sum
dan Case1
nerede F1()
bulunur Sum
gelen Case1
ve her ikisi de Sum1
ve Sum2
gelen Case2
. Bu, daha sonra ikinci algoritma içinde gerçekleşen bir optimizasyon olduğu sonucuna vardığımızda açıklık kazanacaktır.
İlk durum Sum
çağrıları aracılığıyla f(a)
kendi kendine eklenecek yinelemeler f(b)
sonra f(c)
aynı şeyi yapacak ancak f(d)
her 100000
yineleme için kendine ekleyecek çağrılar . İkinci durumda, elimizdeki Sum1
ve Sum2
aynı işlevi arka arkaya iki kez çağrıldığını sanki her ikisi de aynı etki yaptığını göstermektedir.
Bu durumda biz tedavi edebilir Sum1
ve Sum2
sadece düz eski olarak Sum
nerede Sum
bu durumda böyle görünüyor: Sum n=1 : [1,100000] { f(a) = f(a) + f(b); }
şimdi ve biz sadece aynı işlevi olarak düşünebilirsiniz bir optimizasyon gibi bu görünüyor.
Analoji ile Özet
İkinci durumda gördüklerimizle birlikte, her iki döngü için de aynı imzası olduğu için neredeyse bir optimizasyon varmış gibi görünüyor, ancak bu gerçek sorun değil. Sorun tarafından yapılıyor işi değil f(a)
, f(b)
, f(c)
vef(d)
. Her iki durumda ve ikisi arasındaki karşılaştırma, Toplama'nın seyahat etmesi gereken mesafedeki farktır, bu da size yürütme süresindeki farkı verir.
Düşünün For Loops
olarak Summations
bir varlık olarak yinelemeleri yaptığı Boss
iki kişiye emir verdiğini A
& B
ve işlerini ete olduğunu C
ve D
sırasıyla ve onlardan bazı paket almak ve bunu iade etmek. Bu benzetmede, for döngüler veya toplama yinelemeleri ve durum kontrolleri kendilerini gerçekten temsil etmez Boss
. Aslında Boss
, gerçek matematiksel algoritmalardan doğrudan değil, rutin veya altyordam, yöntem, işlev, çeviri birimi vb .'nin gerçek kavramından Scope
ve Code Block
içinde. İlk algoritmanın, 2. algoritmanın ardışık 2 kapsamı olduğu 1 kapsamı vardır.
Her çağrı fişinde İlk durumda içinde, Boss
gider A
ve emir verir ve A
almaya söner B's
sonra paketi Boss
gider C
ve aynı şeyi ve paketi almak için emir verir D
her tekrarında.
İkinci durumda, tüm paketler alınana kadar paketi almak ve almak için Boss
doğrudan çalışır . Sonra tüm paketleri almak için aynı şeyi yapmak için çalışır .A
B's
Boss
C
D's
8 baytlık bir işaretçi ile çalıştığımızdan ve yığın tahsisi ile uğraştığımızdan, aşağıdaki sorunu ele alalım. Diyelim ki Boss
100 metre A
ve A
500 metre C
. İnfazların sırası nedeniyle Boss
başlangıçta ne kadar uzakta olduğu konusunda endişelenmemize gerek yok C
. Her iki durumda da, Boss
başlangıçta geçecek A
ilk önce için B
. Bu benzetme, bu mesafenin kesin olduğunu söylemez; sadece algoritmaların işleyişini göstermek için yararlı bir test senaryosu.
Birçok durumda, yığın ayırmaları yaparken ve önbellek ve sayfa dosyalarıyla çalışırken, adres konumları arasındaki bu mesafeler çok fazla değişmeyebilir veya veri türlerinin doğasına ve dizi boyutlarına bağlı olarak önemli ölçüde değişebilir.
Test Durumları:
Birinci Durum: İlk iterasyonda,Boss
sipariş fişini vermek için başlangıçta 100 feet gitmeliA
veA
gidip bir şey yapmalı, ancak daha sonraona sipariş fişini vermekBoss
için 500 feet seyahatC
etmelidir. Sonra bir sonraki iterasyonda ve diğer her iterasyondaBoss
ikisi arasında 500 feet ileri geri gitmek zorunda.
İkinci Olgu:Boss
ilk tekrarında 100 ayak seyahat etmek olanA
, ama bundan sonra, o zaten orada ve sadece beklerA
tüm slipleri dolana kadar geri kazanmak için. O zamanBoss
ilk iterasyonda 500 feet seyahat etmek zorundaC
çünküC
500 metre uzaklıktadırA
. BuBoss( Summation, For Loop )
, çalıştıktan hemen sonra çağrıldığından,tümsipariş fişleriyapılana kadarA
orada olduğu gibi bekler. A
C's
Seyahat Edilen Mesafeler Arasındaki Fark
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;
Keyfi Değerlerin Karşılaştırılması
600'ün 10 milyondan çok daha az olduğunu kolayca görebiliriz. Şimdi, bu kesin değil, çünkü RAM'in hangi adresi ile her bir yinelemedeki her bir çağrının hangi Önbellek veya Sayfa Dosyasından diğer görünmeyen değişkenlerden kaynaklanacağı arasındaki gerçek farkı bilmiyoruz. Bu sadece farkında olmak ve en kötü senaryodan bakmak için durumun bir değerlendirmesidir.
Bu rakamlardan neredeyse Algoritma Bir Algoritma 99%
İki'den daha yavaş olmalı gibi görünecektir ; Ancak bu sadece Boss's
bir parça veya algoritmaların sorumluluk ve gerçek işçiler dikkate almaz A
, B
, C
, ve D
ve ne her ve Loop her tekrarında yapmak zorundayız. Böylece patronun işi yapılan toplam işin sadece% 15 - 40'ını oluşturur. İşçiler aracılığıyla yapılan işin büyük kısmı, hız oranı farklarının oranını yaklaşık% 50-70'e tutma yönünde biraz daha büyük bir etkiye sahiptir.
Gözlem: - İki algoritma arasındaki farklar
Bu durumda, yapılan işin sürecinin yapısıdır. Durum 2'nin benzer bir işlev beyanı ve tanımına sahip olmanın kısmi optimizasyonundan daha etkili olduğunu göstermekte olup, burada sadece isme ve kat edilen mesafeye göre değişkenler vardır.
Ayrıca, Durum 1'de gidilen toplam mesafenin Durum 2'deki mesafeden çok daha uzak olduğunu görüyoruz ve bu mesafenin Zaman algoritmasını iki algoritma arasında gezdiğini düşünebiliriz . Vaka 1'in Vaka 2'den daha fazla işi vardır .
Bu, ASM
her iki durumda da gösterilen talimatların kanıtlarından gözlemlenebilir . Bu davalar hakkında daha önce ifade edilenlerle birlikte, bu, Durum 1'de patronun her yineleme için tekrar geri dönmeden önce her ikisini de beklemesi A
ve C
geri dönmesi gerektiği gerçeğini açıklamaz A
. Ayrıca son derece uzun zaman alıyorsa A
veya B
o zaman hem Boss
çalışanların hem de diğer çalışanların çalışmayı bekleyen boşta oldukları gerçeğini hesaba katmaz .
In Durumunda 2 tek varlık boşta Boss
işçi dönene kadar. Yani bunun bile algoritma üzerinde bir etkisi var.
OP'lerin Değiştirilmiş Soruları
DÜZENLEME: Sorunun önemsiz olduğu ortaya çıktı, çünkü davranış ciddi bir şekilde dizilerin (n) ve CPU önbelleğinin boyutlarına bağlıdır. Bu yüzden daha fazla ilgi varsa, soruyu yeniden ifade ederim:
Aşağıdaki grafikte beş bölge tarafından gösterildiği gibi farklı önbellek davranışlarına yol açan ayrıntılara ilişkin sağlam bir fikir verebilir misiniz?
Bu CPU'lar için benzer bir grafik sağlayarak CPU / önbellek mimarileri arasındaki farklara dikkat çekmek de ilginç olabilir.
Bu Sorular Hakkında
Şüphesiz gösterdiğim gibi, Donanım ve Yazılım devreye girmeden önce bile altta yatan bir sorun var.
Şimdi, bellek yönetimi ve önbellekleme ile birlikte, hepsi aşağıdakiler arasında entegre bir sistem kümesinde birlikte çalışan sayfa dosyaları vb.
The Architecture
{Donanım, Bellenim, bazı Katıştırılmış Sürücüler, Çekirdekler ve ASM Komut Setleri}.
The OS
{Dosya ve Bellek Yönetim sistemleri, Sürücüler ve Kayıt Defteri}.
The Compiler
{Kaynak Kodun Çeviri Birimleri ve Optimizasyonları}.
- Ve
Source Code
kendine özgü algoritma kümeleriyle bile .
Zaten biz bile herhangi İsteğe bağlı olmak ile herhangi bir makineye uygulamadan önce ilk algoritma dahilinde oluyor bir darboğaz olduğunu görebilirsiniz Architecture
, OS
ve Programmable Language
ikinci algoritmaya göre. Modern bir bilgisayarın içsel özelliklerini dahil etmeden önce zaten bir sorun vardı.
Bitiş Sonuçları
Ancak; bu yeni soruların önemli olmadığını söylemek değildir, çünkü kendileri ve sonuçta bir rol oynarlar. Prosedürleri ve genel performansı etkilerler ve bu, cevaplarını ve yorumlarını veren birçok grafik ve değerlendirmede açıkça görülür.
Eğer benzetmesi dikkat ödemişseniz Boss
ve iki işçi A
ve B
gidip gelen paketleri almak zorunda kaldı C
ve D
sırasıyla söz konusu iki algoritmaların matematiksel gösterimler dikkate; bilgisayar donanım ve yazılımlarının katılımı olmadan görebildiğinizden Case 2
yaklaşık olarak 60%
daha hızlıdır Case 1
.
Bu algoritmalar bazı kaynak kodlara uygulandıktan, derlendi, optimize edildi ve işletim sistemi aracılığıyla belirli bir donanımda işlemlerini gerçekleştirmek için yürütüldükten sonra grafiklere ve çizelgelere baktığınızda, farklılıklar arasında biraz daha fazla bozulma görebilirsiniz. bu algoritmalarda.
Eğer Data
set oldukça küçüktür ilk başta bir fark o kadar feci görünmeyebilir. Ancak bu yana Case 1
hakkındadır 60 - 70%
daha yavaş Case 2
biz zaman infaz farklılıklar açısından bu fonksiyonun büyüme bakabilirsiniz:
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)
Bu yaklaşım, bu iki döngü arasında hem algoritmik olarak hem de yazılım optimizasyonlarını ve makine talimatlarını içeren makine işlemleri arasındaki ortalama farktır.
Veri kümesi doğrusal olarak büyüdüğünde, ikisi arasındaki zaman farkı da artar. Algoritma 1, algoritma 2'den daha fazla getirme özelliğine sahiptir; bu, ilk yinelemeden sonra her yineleme Boss
arasında A
ve C
her yineleme arasında maksimum mesafeyi ileri ve geri hareket etmesi gerektiğinde belirgindir ; Algoritma 2 ise bir kez ve daha sonra Boss
seyahat etmek zorunda A
kaldıktan sonra seyahat etmek A
zorundadır. sadece bir kez maksimum mesafe geçerken A
için C
.
Boss
Aynı anda iki benzer şey yapmaya odaklanmaya çalışmak ve benzer ardışık görevlere odaklanmak yerine ileri geri hokkabazlık yapmaya çalışmak, günün sonunda iki kat daha fazla seyahat etmek ve çalışmak zorunda kaldığı için onu oldukça kızdırır. Bu nedenle, patronunuzun enterpolasyonlu bir darboğazına girmesine izin vererek durumun kapsamını kaybetmeyin, çünkü patronun eşi ve çocukları bunu takdir etmez.
Değişiklik: Yazılım Mühendisliği Tasarım İlkeleri
- Döngüler için yinelemeli Local Stack
ve Heap Allocated
yinelemeli hesaplamalar arasındaki farklar ve kullanımları, verimlilikleri ve etkililikleri arasındaki fark -
Yukarıda önerdiğim matematiksel algoritma, temelde yığın üzerinde tahsis edilen veriler üzerinde işlem yapan döngüler için geçerlidir.
- Ardışık Yığın İşlemleri:
- Döngüler, yığın çerçevesindeki tek bir kod bloğu veya kapsamı içinde veriler üzerinde yerel olarak işlemler gerçekleştiriyorsa, yine de bir çeşit uygulama olur, ancak bellek konumları genellikle sıralı oldukları ve seyahat edilen mesafe veya yürütme süresindeki farkın çok daha yakındır neredeyse ihmal edilebilir. Öbek içinde herhangi bir ayırma yapılmadığından, bellek dağılmaz ve bellek ram yoluyla getirilmez. Bellek genellikle sıralı ve yığın çerçevesi ve yığın işaretçisine göredir.
- Yığın üzerinde ardışık işlemler yapıldığında, modern bir İşlemci yinelenen değerleri ve adresleri yerel önbellek kayıtları içinde tutarak adresleri önbelleğe alır. Buradaki işlemlerin veya talimatların süresi nano saniye düzenindedir.
- Ardışık Yığın Tahsis Edilen İşlemler:
- Yığın ayırmalarını uygulamaya başladığınızda ve işlemcinin CPU, Bus Denetleyici ve Ram modüllerinin mimarisine bağlı olarak ardışık aramalarda bellek adreslerini alması gerektiğinde, işlem veya yürütme süresi mikro milisaniye. Önbelleğe alınmış yığın işlemleriyle karşılaştırıldığında, bunlar oldukça yavaştır.
- CPU'nun bellek adresini Ram'dan alması gerekir ve tipik olarak sistem veri yolundaki her şey CPU'nun içindeki dahili veri yollarına veya veri yollarına kıyasla yavaştır.
Bu nedenle, yığın üzerinde olması gereken verilerle çalışırken ve döngüler halinde dolaşırken, her veri kümesini ve karşılık gelen algoritmaları kendi döngüleri içinde tutmak daha verimlidir. Öbek üzerinde bulunan farklı veri kümelerinin birden çok işlemini tek bir döngüye koyarak ardışık döngüleri etkisizleştirmeye kıyasla daha iyi optimizasyonlar elde edersiniz.
Bunu sıkça önbelleğe alındıkları için yığın üzerinde olan verilerle yapmak uygundur, ancak bellek adresinin her yinelemeyi sorgulaması gereken veriler için uygun değildir.
Yazılım Mühendisliği ve Yazılım Mimarisi Tasarımı devreye giriyor. Verilerinizi nasıl düzenleyeceğinizi, verilerinizi ne zaman önbelleğe alacağınızı, verilerinizi ne zaman öbek üzerinde tahsis edeceğinizi, algoritmalarınızı nasıl tasarlayıp uygulayacağınızı ve bunları ne zaman ve nerede arayacağınızı bilme yeteneğidir.
Aynı veri kümesine ait aynı algoritmaya sahip olabilirsiniz, ancak yalnızca O(n)
çalışma sırasında algoritmanın karmaşıklığından görülen yukarıdaki sorun nedeniyle, yığın varyantı için bir uygulama tasarımı ve diğeri yığın tahsisli varyantı için isteyebilirsiniz. yığın ile.
Yıllar boyunca fark ettiğim kadarıyla, birçok insan bu gerçeği dikkate almaz. Belirli bir veri kümesi üzerinde çalışan bir algoritma tasarlama eğiliminde olacaklar ve veri kümesinin yığın üzerinde yerel olarak önbelleğe alınmasına bakılmaksızın veya yığınta tahsis edildiklerinden bağımsız olarak kullanacaklardır.
Gerçek optimizasyon istiyorsanız, evet kod çoğaltma gibi görünebilir, ancak genelleştirmek için aynı algoritmanın iki varyantına sahip olmak daha verimli olacaktır. Biri yığın işlemleri için, diğeri yinelemeli döngülerde gerçekleştirilen yığın işlemleri için!
İşte sahte bir örnek: İki basit yapı, bir algoritma.
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 psuedo code
// to illustrate a point.
Yığın varyantlarına karşı yığın varyantları için ayrı uygulamalar yaparak bahsettiğim şey budur. Algoritmaların kendileri çok fazla önemli değil, bunları kullanacağınız döngü yapıları.