Geri saymak, saymaktan daha mı hızlı?


131

Bilgisayar bilimi öğretmenimiz bir keresinde, bazı nedenlerden dolayı geri saymanın, saymaktan daha verimli olduğunu söylemişti. Örneğin, bir FOR döngüsü kullanmanız gerekiyorsa ve döngü indeksi bir yerde kullanılmıyorsa (ekrana bir satır N * yazdırmak gibi) şu kodu kastediyorum:

for (i = N; i >= 0; i--)  
  putchar('*');  

daha iyi:

for (i = 0; i < N; i++)  
  putchar('*');  

Gerçekten doğru mu Ve eğer öyleyse, nedenini bilen var mı?


6
Hangi bilgisayar bilimcisi? Hangi yayında?
bmargulies

26
Yinelemede bir nanosaniye veya bir yünlü mamut ailesinde bir saç teli kadar tasarruf edebileceğiniz düşünülebilir. Zamanın putchar% 99,9999'unu kullanıyor (ver veya al).
Mike Dunlavey

38
Erken optimizasyon, tüm kötülüklerin köküdür. Size uygun görünen biçimi kullanın, çünkü (zaten bildiğiniz gibi) mantıksal olarak eşdeğerler. Programlamanın en zor kısmı, programın teorisini diğer programcılara (ve kendinize!) İletmektir. Sizi veya başka bir programcıyı ona bir saniyeden fazla baktıran bir yapı kullanmak net bir kayıptır. "Bu neden geri sayım yapıyor?" Diye düşünerek geçirdiği zamanı asla telafi edemezsiniz.
David M

61
İlk döngü açıkça daha yavaştır, çünkü putchar'ı 11 kez çağırırken, ikincisi onu yalnızca 10 kez çağırır.
Paul Kuliniewicz

17
iİmzasız ise ilk döngünün sonsuz olduğunu fark ettiniz mi?
Shahbaz

Yanıtlar:


371

Bu gerçekten doğru mu ve eğer öyleyse kimse nedenini biliyor mu?

Eski günlerde, bilgisayarlar hala elle erimiş silikadan yontulmuşken, 8 bitlik mikro denetleyiciler Dünya'da dolaşırken ve öğretmeniniz gençken (veya öğretmeninizin öğretmeni gençken), azaltma ve atlama adı verilen ortak bir makine talimatı vardı. sıfır ise (DSZ). Hotshot montaj programcıları döngüleri uygulamak için bu talimatı kullandılar. Daha sonra makineler daha güzel talimatlar aldı, ancak yine de bir şeyi sıfırla karşılaştırmanın başka herhangi bir şeyle karşılaştırmaktan daha ucuz olduğu epeyce işlemci vardı. (Tüm bir kaydı her zaman sıfır olacak şekilde ayıran PPC veya SPARC gibi bazı modern RISC makinelerinde bile doğrudur.)

Öyleyse, döngülerinizi sıfır yerine sıfır ile karşılaştıracak şekilde düzenlerseniz Nne olabilir?

  • Bir kayıt kaydedebilirsiniz
  • Daha küçük bir ikili kodlama ile bir karşılaştırma talimatı alabilirsiniz
  • Önceki bir talimat bir bayrak ayarlamaya gelirse (muhtemelen yalnızca x86 ailesi makinelerde), açık bir karşılaştırma talimatına bile ihtiyacınız olmayabilir.

Bu farklılıklar , modern bir sıra dışı işlemcide gerçek programlarda ölçülebilir bir gelişme ile sonuçlanacak mı? Hiç alışılmadık bir şekilde. Aslında, bir mikro ölçütte bile ölçülebilir bir gelişme gösterebilirseniz etkilenirim.

Özet: Öğretmenin kafasını ters çeviriyorum! Döngüleri nasıl organize edeceğinizle ilgili eskimiş sözde gerçekleri öğrenmemelisiniz. Döngülerle ilgili en önemli şeyin sona erdiğinden , doğru yanıtlar ürettiğinden ve okunması kolay olduğundan emin olmak olduğunu öğrenmelisiniz . Öğretmenin mitolojiye değil önemli şeylere odaklanmasını diliyorum.


3
++ Ve ayrıca, putchardöngü ek yükünden çok daha fazla büyüklük sırası alır.
Mike Dunlavey

41
Bu tam anlamıyla mitoloji değil: eğer bir çeşit uber optimize edilmiş gerçek zamanlı sistem yapıyorsa, işe yarayacaktır. Ancak bu tür bir bilgisayar korsanı muhtemelen tüm bunları zaten biliyordu ve kesinlikle giriş seviyesindeki bilgisayar bilimleri öğrencilerini gizemli ile karıştırmayacaktır.
Paul Nathan

4
@Joshua: Bu optimizasyon ne şekilde tespit edilebilir? Soruyu soranın dediği gibi, döngü indeksi döngünün kendisinde kullanılmaz, bu nedenle yineleme sayısının aynı olması koşuluyla davranışta değişiklik olmaz. Doğruluk kanıtı açısından, değişken ikamesinin yapılması j=N-iiki döngünün eşdeğer olduğunu gösterir.
psmears

7
Özet için +1. Merak etmeyin, çünkü modern donanımda neredeyse hiç fark etmez. 20 yıl önce de neredeyse hiçbir fark yaratmadı. Önemsemeniz gerektiğini düşünüyorsanız, her iki yönde de zamanlayın, net bir fark görmeyin ve kodu açık ve doğru bir şekilde yazmaya geri dönün .
Donal Fellows

3
Özete oy vermeli miyim yoksa olumsuz oy mu vermeliyim bilmiyorum.
Danubian Sailor

29

Derleyicinin kullandığınız sayıların aralığı hakkında ne çıkarabileceğine bağlı olarak bazı donanımlarda neler olabileceği aşağıda açıklanmıştır: artan döngü ile döngü i<Nboyunca her seferinde test etmeniz gerekir . Azalan versiyon için, taşıma bayrağı (çıkarma işleminin bir yan etkisi olarak ayarlanır) size otomatik olarak bunu söyleyebilir i>=0. Bu, döngü boyunca her seferinde bir test tasarrufu sağlar.

Gerçekte, modern boru hatlı işlemci donanımında, talimatlardan saat döngülerine kadar basit bir 1-1 eşleme olmadığından, bu şeyler neredeyse kesinlikle alakasızdır. (Bir mikrodenetleyiciden hassas şekilde zamanlanmış video sinyalleri üretmek gibi şeyler yapıyor olsanız da bunun ortaya çıkacağını hayal edebiliyorum. Ama o zaman yine de montaj dilinde yazarsınız.)


2
Bu sıfır bayrağı olmaz mıydı, taşıma bayrağı değil mi?
Bob

2
@Bob Bu durumda, sıfıra ulaşmak, bir sonuç yazdırmak, daha fazla düşürmek ve ardından sıfırın altına indiğinizi görmek (veya ödünç almaya) neden olmak isteyebilirsiniz. Ancak biraz farklı yazıldığında azalan bir döngü bunun yerine sıfır bayrağını kullanabilir.
sigfpe

1
Tamamen bilgiçlik taslamak gerekirse, tüm modern donanımlar ardışık düzende değildir. Gömülü işlemciler, bu tür bir mikrooptimizasyonla çok daha alakalı olacak.
Paul Nathan

@Paul Atmel AVR'lerle ilgili deneyimlerim olduğu için mikrodenetleyicilerden bahsetmeyi unutmadım ...
sigfpe

27

Intel x86 komut setinde, sıfıra doğru geri saymak için bir döngü oluşturmak genellikle sıfır olmayan bir çıkış koşuluna kadar sayan bir döngüden daha az komutla yapılabilir. Özellikle, ECX kaydı geleneksel olarak x86 asm'de bir döngü sayacı olarak kullanılır ve Intel komut setinde, testin sonucuna göre ECX kaydını sıfır ve sıçramalar için test eden özel bir jcxz atlama talimatı vardır.

Ancak, döngünüz saat döngüsü sayılarına karşı çok hassas olmadığı sürece performans farkı ihmal edilebilir olacaktır. Sıfıra doğru geri saymak, saymaya kıyasla döngünün her yinelemesinde 4 veya 5 saat döngüsünü azaltabilir, bu nedenle gerçekten kullanışlı bir teknikten çok bir yeniliktir.

Ayrıca, bu günlerde iyi bir optimizasyon derleyicisi, sayma döngüsü kaynak kodunuzu geri sayımdan sıfır makine koduna dönüştürebilmelidir (döngü indeksi değişkenini nasıl kullandığınıza bağlı olarak), bu nedenle döngülerinizi yazmak için herhangi bir neden yoktur. burada ve orada bir veya iki döngüyü sıkıştırmanın garip yolları.


2
Birkaç yıl önce Microsoft'un C ++ derleyicisinin bu optimizasyonu yaptığını gördüm. Döngü dizininin kullanılmadığını görebilir, bu nedenle onu en hızlı şekilde yeniden düzenler.
Mark Ransom

1
@Mark: 1996'da başlayan Delphi derleyicisi de.
dthorpe

4
@MarkRansom Aslında, derleyici, döngüde nasıl kullanıldığına bağlı olarak döngü indeksi değişkeni kullanılsa bile geri sayımı kullanarak döngüyü uygulayabilir. Döngü indeksi değişkeni yalnızca statik dizileri indekslemek için kullanılıyorsa (derleme zamanında bilinen boyuttaki diziler), dizi indekslemesi, x86'da hala tek bir talimat olabilen ptr + dizi boyutu - döngü indeksi değişkeni olarak yapılabilir. Birleştiricinin hatalarını ayıklamak ve döngünün geri sayımını görmek oldukça vahşi ama dizi indeksleri yükseliyor!
dthorpe

1
Aslında bugün derleyiciniz muhtemelen bir dec / jnz çiftinden daha yavaş olduklarından loop ve jecxz komutlarını kullanmayacaktır.
2013

1
@FUZxxl Döngünüzü garip şekillerde yazmamak için daha fazla neden. İnsan tarafından okunabilir açık kod yazın ve derleyicinin işini yapmasına izin verin.
dthorpe

23

Evet..!!

N'den 0'a kadar saymak, donanımın karşılaştırmayı nasıl yapacağı anlamında 0'dan N'ye saymaktan biraz daha hızlıdır.

Not karşılaştırma her döngüde

i>=0
i<N

Çoğu işlemcinin sıfır talimatı ile karşılaştırması vardır .. dolayısıyla ilki şu şekilde makine koduna çevrilecektir:

  1. İ yükle
  2. Sıfırdan Küçük veya Eşit ise karşılaştırın ve atlayın

Ancak ikincisinin her seferinde N form Belleği yüklemesi gerekir

  1. i yükle
  2. yük N
  3. Sub i ve N
  4. Sıfırdan Küçük veya Eşit ise karşılaştırın ve atlayın

Yani bu, geri sayımdan veya arttığından değil .. Kodunuzun makine koduna nasıl çevrileceğinden dolayı ..

Yani 10'dan 100'e kadar saymak, 100'den 10'a kadar saymakla
aynıdır.Ama i = 100'den 0'a saymak, i = 0'dan 100'e kadar daha hızlıdır - çoğu durumda
ve i = N'den 0'a saymak, i = 'den daha hızlıdır. 0 - N

  • Günümüzde derleyicilerin bu optimizasyonu sizin için yapabileceğini unutmayın (eğer yeterince akıllıysa)
  • Ayrıca, boru hattının Belady'nin anormallik benzeri etkisine neden olabileceğini unutmayın (neyin daha iyi olacağından emin olamıyorum)
  • Son olarak: lütfen sunduğunuz 2 for döngüsünün eşdeğer olmadığını unutmayın .. ilk baskı bir tane daha * ....

İlgili: n ++ neden n = n + 1'den daha hızlı çalışıyor?


6
yani geri saymanın daha hızlı olmadığını, sıfırla karşılaştırmanın diğer değerlerden daha hızlı olduğunu söylüyorsunuz. Anlamı 10'dan 100'e kadar saymak ve 100'den 10'a geri saymak aynı mı olurdu?
Bob

8
Evet .. mesele "geri sayma veya
artma

3
Bu doğru olsa da montajcı seviyesi. Gerçekte gerçeği söylemek için iki şey bir araya geliyor - uzun borular ve spekülatif talimatlar kullanan modern donanım, fazladan bir döngü oluşturmadan "Sub i ve N" ye gizlice girecek ve - en kaba derleyici bile "Sub i ve N "yok.
James Anderson

2
@nico eski bir sistem olmak zorunda değil. Sadece, kayıt değerine eşdeğerden daha hızlı / daha iyi olan sıfır işlemle karşılaştırmanın olduğu bir komut kümesi olmalıdır. x86, jcxz'de var. x64'te hala var. Eski değil. Ayrıca, RISC mimarileri genellikle özel durum sıfırdır. Örneğin, DEC AXP Alpha yongası (MIPS ailesinde), "sıfır kaydı" na sahiptir - sıfır olarak okunur, yazma hiçbir şey yapmaz. Sıfır değeri içeren genel bir kayıt yerine sıfır yazmacı ile karşılaştırma, talimatlar arası bağımlılıkları azaltır ve sıra dışı yürütmeye yardımcı olur.
dthorpe

5
@Betamoo: Sık sık neden daha iyi / daha doğru cevapların (sizinki) daha fazla oy tarafından daha fazla takdir edilmediğini merak ediyorum ve yığın aşımı oylarının çok sık olarak cevap veren bir kişinin itibarından (puan olarak) etkilendiği sonucuna varıyorum ( bu çok çok kötü) ve cevabın doğruluğundan değil
Artur

12

C'de psudo montajına:

for (i = 0; i < 10; i++) {
    foo(i);
}

dönüşür

    clear i
top_of_loop:
    call foo
    increment i
    compare 10, i
    jump_less top_of_loop

süre:

for (i = 10; i >= 0; i--) {
    foo(i);
}

dönüşür

    load i, 10
top_of_loop:
    call foo
    decrement i
    jump_not_neg top_of_loop

İkinci psudo montajında ​​karşılaştırma eksikliğine dikkat edin. Birçok mimaride, atlamalar için kullanabileceğiniz aritmatik işlemlerle (toplama, çıkarma, çarpma, bölme, artırma, azaltma) ayarlanan bayraklar vardır. Bunlar genellikle size işlemin sonucunun 0 ile ücretsiz olarak karşılaştırılmasını sağlar. Aslında birçok mimaride

x = x - 0

anlamsal olarak aynıdır

compare x, 0

Ayrıca, örneğimdeki 10 ile karşılaştırma daha kötü kodla sonuçlanabilir. 10'un bir kasada yaşaması gerekebilir, bu nedenle, bu maliyetler yetersizse ve her seferinde 10'u döngü boyunca hareket ettirmek veya yeniden yüklemek için fazladan kodla sonuçlanabilir.

Derleyiciler bazen bundan yararlanmak için kodu yeniden düzenleyebilirler, ancak çoğu zaman zordur çünkü döngü boyunca yönü tersine çevirmenin anlamsal olarak eşdeğer olduğundan emin olamazlar.


Sadece 1 yerine 2 komuttan oluşan bir fark olması mümkün mü?
Pacerier

Ayrıca, bundan emin olmak neden zor? Var i, döngü içinde kullanılmadığı sürece , açıkça ters çevirebilirsiniz, değil mi?
Pacerier

6

Böyle bir durumda daha hızlı geri sayın:

for (i = someObject.getAllObjects.size(); i >= 0; i--) {…}

çünkü someObject.getAllObjects.size()başlangıçta bir kez çalıştırılır.


Elbette, size()Peter'ın bahsettiği gibi, döngüden seslenerek benzer davranışlar elde edilebilir :

size = someObject.getAllObjects.size();
for (i = 0; i < size; i++) {…}

5
"Kesinlikle daha hızlı" değil. Çoğu durumda, bu size () çağrısı yukarı doğru sayılırken döngüden çıkarılabilir, bu nedenle yine de yalnızca bir kez çağrılır. Açıkçası bu, dile ve derleyiciye bağlıdır (ve koda bağlıdır; örneğin, C ++ 'da, size () sanal ise kaldırılmaz), ancak her iki şekilde de kesin olmaktan uzaktır.
Peter

3
@Peter: Yalnızca derleyici bu boyutun () döngü boyunca idempotent olduğunu kesin olarak bilirse. Döngü çok basit olmadığı sürece muhtemelen durum her zaman böyle değildir .
Lawrence Dol

@LawrenceDol, Compilatino kullanan dinamik kodunuz yoksa derleyici bunu kesinlikle bilecektir exec.
Pacerier

4

Geri saymak yukarıdan daha hızlı mı?

Olabilir. Ancak, zamanın% 99'undan çok daha fazlası önemli olmayacak, bu nedenle döngüyü sonlandırmak için en 'mantıklı' testi kullanmalısınız ve mantıklı bir şekilde, bir okuyucunun anlamasının en az miktarda düşünmesini gerektirdiğini kastediyorum. Döngünün ne yaptığı (onu durduran şey dahil). Kodunuzun, kodun ne yaptığının zihinsel (veya belgelenmiş) modeliyle eşleşmesini sağlayın.

Döngü çalışıyorsa, bir dizinin (veya listenin veya her neyse) yukarı doğru ilerlerse, artan bir sayaç genellikle okuyucunun döngünün ne yaptığını nasıl düşündüğüyle daha iyi eşleşir - döngünüzü bu şekilde kodlayın.

Ancak, Nöğeleri olan bir kapta çalışıyor ve ilerledikçe öğeleri kaldırıyorsanız, sayacı aşağı çekmek daha bilişsel bir anlam ifade edebilir.

Cevaptaki 'belki' hakkında biraz daha detay:

Çoğu mimaride sıfırla sonuçlanan (veya sıfırdan negatife giden) bir hesaplama için testin açık bir test talimatı gerektirmediği doğrudur - sonuç doğrudan kontrol edilebilir. Bir hesaplamanın başka bir sayı ile sonuçlanıp sonuçlanmadığını test etmek istiyorsanız, talimat akışının genellikle bu değeri test etmek için açık bir talimat içermesi gerekir. Bununla birlikte, özellikle modern CPU'larda, bu test genellikle bir döngü yapısına gürültü seviyesinden daha az ek süre ekler. Özellikle bu döngü G / Ç gerçekleştiriyorsa.

Öte yandan, sıfırdan geri sayarsanız ve sayacı bir dizi indeksi olarak kullanırsanız, örneğin, kodun sistemin bellek mimarisine karşı çalıştığını görebilirsiniz - bellek okumaları genellikle bir önbelleğin 'ileriye bakmasına' neden olur. sıralı bir okuma beklentisiyle mevcut olanı geçen birkaç hafıza konumu. Bellek yoluyla geriye doğru çalışıyorsanız, önbelleğe alma sistemi, daha düşük bir bellek adresindeki bir bellek konumunun okunmasını beklemeyebilir. Bu durumda, 'geriye doğru' dönmenin performansa zarar vermesi mümkündür. Bununla birlikte, yine de döngüyü bu şekilde kodlardım (performans bir sorun haline gelmediği sürece) çünkü doğruluk çok önemlidir ve kodun bir modelle eşleşmesini sağlamak, doğruluğun sağlanmasına yardımcı olmak için harika bir yoldur. Yanlış kod, alabildiğiniz kadar optimize edilmemiştir.

Bu yüzden, kodun performansı gerçekten önemli olmadığı sürece, profesörün tavsiyesini unutmaya meyilliyim (tabii ki, testinde değil - sınıfın gittiği yere kadar hala pragmatik olmalısın).


3

Bazı eski CPU'larda DJNZ== "azaltma ve sıfır değilse atla" gibi komutlar vardı / vardı . Bu, bir ilk sayım değerini bir yazmacıya yüklediğiniz ve daha sonra tek bir komutla azalan bir döngüyü etkili bir şekilde yönetebileceğiniz verimli döngülere izin verdi. Burada 1980'lerin ISA'sından bahsediyoruz - öğretmeniniz bu "pratik kuralın" modern CPU'lar için hala geçerli olduğunu düşünüyorsa, ciddi şekilde iletişim kuramaz.


3

Bob,

Mikrooptimizasyonlar yapana kadar, bu noktada CPU'nuz için el kitabına sahip olacaksınız. Dahası, bu tür bir şey yapıyor olsaydınız, muhtemelen bu soruyu sormanıza gerek kalmazdı. :-) Ama belli ki öğretmenin bu fikre katılmıyor ...

Döngü örneğinizde dikkate almanız gereken 4 şey var:

for (i=N; 
 i>=0;             //thing 1
 i--)             //thing 2
{
  putchar('*');   //thing 3
}
  • karşılaştırma

Karşılaştırma (diğerlerinin de belirttiği gibi) belirli işlemci mimarileri ile ilgilidir . Windows çalıştıranlardan daha fazla işlemci türü vardır. Özellikle, 0 ile karşılaştırmaları basitleştiren ve hızlandıran bir talimat olabilir.

  • ayarlama

Bazı durumlarda, yukarı veya aşağı ayarlamak daha hızlıdır. Tipik olarak iyi bir derleyici bunu çözer ve yapabiliyorsa döngüyü yeniden yapar. Yine de tüm derleyiciler iyi değildir.

  • Döngü Gövdesi

Putchar ile bir sistem çağrısına erişiyorsunuz. Bu çok yavaş. Artı, ekranda görüntü oluşturuyorsunuz (dolaylı olarak). Bu daha da yavaştır. 1000: 1 oranını veya daha fazlasını düşünün. Bu durumda, döngü gövdesi, döngü ayarlama / karşılaştırmasının maliyetinden tamamen ve tamamen ağır basar.

  • önbellekler

Önbellek ve bellek düzeninin performans üzerinde büyük bir etkisi olabilir. Bu durumda önemli değil. Ancak, bir diziye erişiyorsanız ve optimum performansa ihtiyacınız varsa, derleyicinizin ve işlemcinizin bellek erişimlerini nasıl düzenlediğini araştırmanız ve bundan en iyi şekilde yararlanmak için yazılımınızı ayarlamanız size düşecektir. Stok örneği, matris çarpımı ile ilgili olarak verilen örnektir.


3

Sayacınızı artırmanızdan veya azaltmanızdan çok daha önemli olan, hafızayı yükseltip yükseltmediğinizdir. Çoğu önbellek, hafızayı azaltmak için değil, hafızayı artırmak için optimize edilmiştir. Bellek erişim süresi, günümüzde çoğu programın karşılaştığı darboğaz olduğundan, bu, programınızı değiştirerek belleğinizi yükseltmenin, sayacınızı sıfır olmayan bir değerle karşılaştırmayı gerektirse bile bir performans artışı sağlayabileceği anlamına gelir. Bazı programlarımda, kodumu hafızayı düşürmek yerine yukarı çıkacak şekilde değiştirerek performansta önemli bir gelişme gördüm.

Şüpheci? Hafızada yukarı / aşağı giden zaman döngülerine bir program yazmanız yeterlidir. İşte aldığım çıktı:

Average Up Memory   = 4839 mus
Average Down Memory = 5552 mus

Average Up Memory   = 18638 mus
Average Down Memory = 19053 mus

(burada "mus", mikrosaniye anlamına gelir) bu programı çalıştırdığınızda:

#include <chrono>
#include <iostream>
#include <random>
#include <vector>

//Sum all numbers going up memory.
template<class Iterator, class T>
inline void sum_abs_up(Iterator first, Iterator one_past_last, T &total) {
  T sum = 0;
  auto it = first;
  do {
    sum += *it;
    it++;
  } while (it != one_past_last);
  total += sum;
}

//Sum all numbers going down memory.
template<class Iterator, class T>
inline void sum_abs_down(Iterator first, Iterator one_past_last, T &total) {
  T sum = 0;
  auto it = one_past_last;
  do {
    it--;
    sum += *it;
  } while (it != first);
  total += sum;
}

//Time how long it takes to make num_repititions identical calls to sum_abs_down().
//We will divide this time by num_repitions to get the average time.
template<class T>
std::chrono::nanoseconds TimeDown(std::vector<T> &vec, const std::vector<T> &vec_original,
                                  std::size_t num_repititions, T &running_sum) {
  std::chrono::nanoseconds total{0};
  for (std::size_t i = 0; i < num_repititions; i++) {
    auto start_time = std::chrono::high_resolution_clock::now();
    sum_abs_down(vec.begin(), vec.end(), running_sum);
    total += std::chrono::high_resolution_clock::now() - start_time;
    vec = vec_original;
  }
  return total;
}

template<class T>
std::chrono::nanoseconds TimeUp(std::vector<T> &vec, const std::vector<T> &vec_original,
                                std::size_t num_repititions, T &running_sum) {
  std::chrono::nanoseconds total{0};
  for (std::size_t i = 0; i < num_repititions; i++) {
    auto start_time = std::chrono::high_resolution_clock::now();
    sum_abs_up(vec.begin(), vec.end(), running_sum);
    total += std::chrono::high_resolution_clock::now() - start_time;
    vec = vec_original;
  }
  return total;
}

template<class Iterator, typename T>
void FillWithRandomNumbers(Iterator start, Iterator one_past_end, T a, T b) {
  std::random_device rnd_device;
  std::mt19937 generator(rnd_device());
  std::uniform_int_distribution<T> dist(a, b);
  for (auto it = start; it != one_past_end; it++)
    *it = dist(generator);
  return ;
}

template<class Iterator>
void FillWithRandomNumbers(Iterator start, Iterator one_past_end, double a, double b) {
  std::random_device rnd_device;
  std::mt19937_64 generator(rnd_device());
  std::uniform_real_distribution<double> dist(a, b);
  for (auto it = start; it != one_past_end; it++)
    *it = dist(generator);
  return ;
}

template<class ValueType>
void TimeFunctions(std::size_t num_repititions, std::size_t vec_size = (1u << 24)) {
  auto lower = std::numeric_limits<ValueType>::min();
  auto upper = std::numeric_limits<ValueType>::max();
  std::vector<ValueType> vec(vec_size);

  FillWithRandomNumbers(vec.begin(), vec.end(), lower, upper);
  const auto vec_original = vec;
  ValueType sum_up = 0, sum_down = 0;

  auto time_up   = TimeUp(vec, vec_original, num_repititions, sum_up).count();
  auto time_down = TimeDown(vec, vec_original, num_repititions, sum_down).count();
  std::cout << "Average Up Memory   = " << time_up/(num_repititions * 1000) << " mus\n";
  std::cout << "Average Down Memory = " << time_down/(num_repititions * 1000) << " mus"
            << std::endl;
  return ;
}

int main() {
  std::size_t num_repititions = 1 << 10;
  TimeFunctions<int>(num_repititions);
  std::cout << '\n';
  TimeFunctions<double>(num_repititions);
  return 0;
}

Hem sum_abs_upve sum_abs_downaynı şeyi (sayı vektör toplamı) yapmak ve tek fark varlık ile aynı şekilde zamanlanmış sum_abs_upise bellek gider sum_abs_downbelleğe iner. Her veciki işlevin de aynı bellek konumlarına erişmesi için referans olarak bile geçiyorum . Yine de, sum_abs_uptutarlı bir şekilde daha hızlıdır sum_abs_down. Kendiniz çalıştırın (g ++ -O3 ile derledim).

Zamanlamamın ne kadar sıkı olduğuna dikkat etmek önemlidir. Bir döngünün gövdesi büyükse, döngü gövdesini yürütmek için gereken süre büyük olasılıkla tamamen hakim olacağından, yineleyicinin hafızada yukarı veya aşağı gidip gitmediği önemli değildir. Ayrıca, bazı nadir döngülerle hafızanın aşağı inmesinin bazen yukarı çıkmaktan daha hızlı olduğunu belirtmek önemlidir . Ancak bu tür döngülerde bile, hafızanın yukarı çıkması her zaman aşağı inmekten daha yavaş olmadı (hafızayı yükselten küçük gövdeli döngülerin aksine, bunun tersi genellikle doğrudur; aslında, küçük bir avuç döngü için I ' Zamanlanmış, bellek yukarı çıkarak performans artışı% 40 + olmuştur).

Temel bir kural olarak, seçeneğiniz varsa, döngünün gövdesi küçükse ve döngünüzün hafızayı aşağı çekmek yerine yukarı çıkması arasında çok az fark varsa, o zaman hafızayı yükseltmelisiniz.

Bilginize vec_original, deneyler yapmak, değişmeyi kolaylaştırmak sum_abs_upve bu değişikliklerin gelecekteki zamanlamaları etkilemesine izin sum_abs_downvermezken onları değiştirecek bir şekilde oradadır vec. Çok uğraşırken tavsiye sum_abs_upve sum_abs_downve sonuçları zamanlama.


2

yönü ne olursa olsun her zaman önek formunu kullanın (i ++ yerine ++ i)!

for (i=N; i>=0; --i)  

veya

for (i=0; i<N; ++i) 

Açıklama: http://www.eskimo.com/~scs/cclass/notes/sx7b.html

Ayrıca yazabilirsiniz

for (i=N; i; --i)  

Ancak modern derleyicilerin bu optimizasyonları tam olarak yapabilmesini beklerdim.


Daha önce hiç kimsenin bundan şikayet ettiğini görmemiştim. Ancak bağlantıyı okuduktan sonra aslında mantıklı :) Teşekkürler.
Tommy Jakobsen

3
Um, neden her zaman önek formunu kullansın? Devam eden bir ödev yoksa, bunlar aynıdır ve bağlantı kurduğunuz makale postfix formunun daha yaygın olduğunu söylüyor.
bobDevil

3
Neden her zaman önek formu kullanılmalı? Bu örnekte, anlamsal olarak aynıdır.
Ben Zotto

2
Sonek formu potansiyel olarak nesnenin gereksiz bir kopyasını oluşturabilir, ancak değer hiç kullanılmıyorsa, derleyici muhtemelen onu önek formuna optimize edecektir.
Nick Lewis

Alışkanlıktan dolayı, her zaman --i ve i ++ yapıyorum çünkü C bilgisayarlarını öğrendiğimde genellikle bir kayıt öncesi ve sonradan artırma vardı, ancak bunun tersi geçerli değildi. Bu nedenle, * p ++ ve * - p, * ++ p ve * p - 'den daha hızlıydı çünkü ilk ikisi tek bir 68000 makine kodu komutunda yapılabilir.
JeremyP

2

Bu ilginç bir soru, ancak pratik bir konu olarak önemli olduğunu ve bir döngüyü diğerinden daha iyi yapmadığını düşünüyorum.

Bu wikipedia sayfasına göre: Artık saniye , "... güneş günü, esas olarak gelgit sürtünmesi nedeniyle her yüzyılda 1,7 ms daha uzar." Ama doğum gününüze kadar günleri sayıyorsanız, zamandaki bu küçük farkı gerçekten önemsiyor musunuz?

Kaynak kodunun kolay okunması ve anlaşılması daha önemlidir. Bu iki döngü, okunabilirliğin neden önemli olduğuna dair iyi bir örnektir - aynı sayıda döngü yapmazlar.

Çoğu programcının okuduğuna (i = 0; i <N; i ++) ve bunun N kez döngü yaptığını hemen anladığına bahse girerim. Bir döngü (i = 1; i <= N; i ++) benim için biraz daha az net ve (i = N; i> 0; i--) ile bir an düşünmem gerekiyor . En iyisi, kodun amacının herhangi bir düşünme gerekmeden doğrudan beyne girmesidir.


Her iki yapının da anlaşılması tamamen kolaydır. Eğer 3 veya 4 tekrarınız varsa, bir döngü yapmaktansa talimatı kopyalamanın daha iyi olduğunu iddia eden bazı insanlar var çünkü bu onlar için daha kolay anlaşılır.
Danubian Sailor

2

Garip bir şekilde, bir fark varmış gibi görünüyor. En azından PHP'de. Aşağıdaki kıyaslamayı düşünün:

<?php

print "<br>".PHP_VERSION;
$iter = 100000000;
$i=$t1=$t2=0;

$t1 = microtime(true);
for($i=0;$i<$iter;$i++){}
$t2 = microtime(true);
print '<br>$i++ : '.($t2-$t1);

$t1 = microtime(true);
for($i=$iter;$i>0;$i--){}
$t2 = microtime(true);
print '<br>$i-- : '.($t2-$t1);

$t1 = microtime(true);
for($i=0;$i<$iter;++$i){}
$t2 = microtime(true);
print '<br>++$i : '.($t2-$t1);

$t1 = microtime(true);
for($i=$iter;$i>0;--$i){}
$t2 = microtime(true);
print '<br>--$i : '.($t2-$t1);

Sonuçlar ilginç:

PHP 5.2.13
$i++ : 8.8842368125916
$i-- : 8.1797409057617
++$i : 8.0271911621094
--$i : 7.1027431488037


PHP 5.3.1
$i++ : 8.9625310897827
$i-- : 8.5790238380432
++$i : 5.9647901058197
--$i : 5.4021768569946

Birisi nedenini bilirse bilmek güzel olur :)

DÜZENLE : 0'dan değil, diğer keyfi değerlerden saymaya başlasanız bile sonuçlar aynıdır. Yani muhtemelen fark yaratan sıfırla karşılaştırma yoktur?


Daha yavaş olmasının nedeni, önek operatörünün bir geçici depolamaya gerek olmamasıdır. $ Foo = $ i ++; Üç şey olur: $ i geçici olarak depolanır, $ i artırılır ve sonra $ foo bu geçici değerin değerine atanır. $ İ ++ durumunda; akıllı bir derleyici geçicinin gereksiz olduğunu fark edebilir. PHP sadece yapmaz. C ++ ve Java derleyicileri, bu basit optimizasyonu yapacak kadar akıllıdır.
Conspicuous Compiler

ve neden $ i-- $ i ++ 'dan daha hızlıdır?
ts.

Kriterinizin kaç yinelemesini çalıştırdınız? Önde gidenleri kesip her sonuç için bir ortalama aldınız mı? Bilgisayarınız testler sırasında başka bir şey yapıyor muydu? Bu ~ 0.5 fark sadece diğer CPU aktivitesinin veya boru hattı kullanımının bir sonucu olabilir veya ... veya ... peki, fikri anladınız.
Eight-Bit Guru

Evet, burada ortalamalar veriyorum. Kıyaslama farklı makinelerde çalıştırıldı ve fark tesadüfi.
ts.

@Conspicuous Compiler => biliyor musunuz veya sanıyorsunuz?
ts.

2

Daha hızlı olabilir .

Şu anda çalıştığım NIOS II işlemci üzerinde, geleneksel for döngüsü

for(i=0;i<100;i++)

montajı üretir:

ldw r2,-3340(fp) %load i to r2
addi r2,r2,1     %increase i by 1
stw r2,-3340(fp) %save value of i
ldw r2,-3340(fp) %load value again (???)
cmplti r2,r2,100 %compare if less than equal 100
bne r2,zero,0xa018 %jump

Geri sayarsak

for(i=100;i--;)

2 talimata daha az ihtiyaç duyan bir montaj elde ediyoruz.

ldw r2,-3340(fp)
addi r3,r2,-1
stw r3,-3340(fp)
bne r2,zero,0xa01c

İç döngünün çok yürütüldüğü iç içe döngülerimiz varsa, ölçülebilir bir farkımız olabilir:

int i,j,a=0;
for(i=100;i--;){
    for(j=10000;j--;){
        a = j+1;
    }
}

İç döngü yukarıdaki gibi yazılırsa, yürütme süresi: 0.12199999999999999734 saniyedir. İç döngü geleneksel şekilde yazılırsa, yürütme süresi: 0.17199999999999998623 saniye. Yani döngü geri sayımı yaklaşık % 30 daha hızlıdır.

Ancak: bu test tüm GCC optimizasyonları kapalıyken yapılmıştır. Onları açarsak, derleyici aslında bu pratik optimizasyondan daha akıllıdır ve hatta tüm döngü boyunca değeri bir kayıt defterinde tutar ve şöyle bir derleme elde ederiz

addi r2,r2,-1
bne r2,zero,0xa01c

Bu özel örnekte derleyici, a değişkeninin döngü yürütmeden sonra her zaman 1 olacağını ve döngüleri tamamen atladığını fark eder.

Bununla birlikte, bazen döngü gövdesi yeterince karmaşıksa, derleyicinin bu optimizasyonu yapamayacağını, bu nedenle her zaman hızlı bir döngü yürütme elde etmenin en güvenli yolunun yazmak olduğunu deneyimledim:

register int i;
for(i=10000;i--;)
{ ... }

Elbette bu yalnızca döngünün ters yönde yürütülmesi önemli değilse ve Betamoo'nun dediği gibi, yalnızca sıfıra doğru geri sayıyorsanız işe yarar .


2

Öğretmenin söylediği, çok fazla açıklama yapmadan dolaylı bir ifadeydi. Azaltmanın artmaktan daha hızlı olduğu DEĞİLDİR, ancak azaltma ile artıştan çok daha hızlı döngü oluşturabilirsiniz.

Bu konuda uzun uzadıya durmadan, döngü sayacı vb. Kullanmaya gerek kalmadan - aşağıda önemli olan sadece hız ve döngü sayısıdır (sıfır olmayan).

Çoğu insan 10 yinelemeli döngüyü şu şekilde uygular:

int i;
for (i = 0; i < 10; i++)
{
    //something here
}

Vakaların% 99'u için tek ihtiyaç duyulabilir ancak PHP, PYTHON, JavaScript ile birlikte, CPU işaretlerinin gerçekten önemli olduğu zaman açısından kritik yazılımlar (genellikle gömülü, işletim sistemi, oyunlar vb.) Vardır, bu nedenle kısaca montaj koduna bakın:

int i;
for (i = 0; i < 10; i++)
{
    //something here
}

derlemeden sonra (optimizasyon olmadan) derlenmiş sürüm şöyle görünebilir (VS2015):

-------- C7 45 B0 00 00 00 00  mov         dword ptr [i],0  
-------- EB 09                 jmp         labelB 
labelA   8B 45 B0              mov         eax,dword ptr [i]  
-------- 83 C0 01              add         eax,1  
-------- 89 45 B0              mov         dword ptr [i],eax  
labelB   83 7D B0 0A           cmp         dword ptr [i],0Ah  
-------- 7D 02                 jge         out1 
-------- EB EF                 jmp         labelA  
out1:

Tüm döngü 8 talimattır (26 bayt). İçinde - aslında 2 dallı 6 talimat (17 bayt) var. Evet evet daha iyi yapılabileceğini biliyorum (bu sadece bir örnek).

Şimdi, genellikle gömülü geliştirici tarafından yazılmış bulacağınız bu sık yapıyı düşünün:

i = 10;
do
{
    //something here
} while (--i);

Ayrıca 10 kez yinelenir (evet, değerin döngü için gösterilenle karşılaştırıldığında farklı olduğunu biliyorum, ancak burada yineleme sayısını önemsiyoruz). Bu, şu şekilde derlenebilir:

00074EBC C7 45 B0 01 00 00 00 mov         dword ptr [i],1  
00074EC3 8B 45 B0             mov         eax,dword ptr [i]  
00074EC6 83 E8 01             sub         eax,1  
00074EC9 89 45 B0             mov         dword ptr [i],eax  
00074ECC 75 F5                jne         main+0C3h (074EC3h)  

5 talimat (18 bayt) ve sadece bir dal. Aslında döngüde 4 komut vardır (11 bayt).

En iyisi, bazı CPU'ların (x86 / x64 uyumlu dahil) bir kaydı azaltan, daha sonra sonucu sıfırla karşılaştıran ve sonuç sıfırdan farklıysa dallanma gerçekleştiren talimatlara sahip olmasıdır. Neredeyse TÜM PC CPU'lar bu talimatı uygular. Bunu kullanarak döngü aslında sadece bir (evet bir) 2 baytlık talimattır:

00144ECE B9 0A 00 00 00       mov         ecx,0Ah  
label:
                          // something here
00144ED3 E2 FE                loop        label (0144ED3h)  // decrement ecx and jump to label if not zero

Hangisinin daha hızlı olduğunu açıklamam gerekiyor mu?

Şimdi, belirli bir CPU yukarıdaki talimatı uygulamasa bile, bunu taklit etmek için gereken tek şey bir azalmadır ve önceki talimatın sonucu sıfır olursa koşullu atlamadır.

Öyleyse, bazı durumlardan bağımsız olarak, neden yanıldığımı vb. Bir yorum olarak gösterebilirsiniz. VURUŞUYORUM - EVET, nasıl, neden ve ne zaman olduğunu biliyorsanız, DÜŞÜRMEK FAYDALIDIR.

PS. Evet, bilge derleyicinin (uygun optimizasyon düzeyine sahip) döngü için yeniden yazacağını (artan döngü sayacı ile) do'ya yeniden yazacağını biliyorum, bu arada sabit döngü yinelemeleri için eşdeğer ... (veya kaydı sil) ...


1

Hayır, bu gerçekten doğru değil. Daha hızlı olabileceği bir durum, aksi takdirde bir döngünün her yinelemesi sırasında sınırları kontrol etmek için bir işlevi çağıracağınız zamandır.

for(int i=myCollection.size(); i >= 0; i--)
{
   ...
}

Ancak bunu bu şekilde yapmak daha az anlaşılırsa, buna değmez. Modern dillerde, mümkünse bir foreach döngüsü kullanmalısınız. Dizine ihtiyacınız olmadığında, foreach döngüsü kullanmanız gereken durumu özellikle belirtiyorsunuz.


1
Açık ve verimli olmak için en azından alışkanlık içinde olmalısınız for(int i=0, siz=myCollection.size(); i<siz; i++).
Lawrence Dol

1

Mesele şu ki, geri sayarken, azaltmayı i >= 0ayrı ayrı kontrol etmenize gerek yok i. Gözlemek:

for (i = 5; i--;) {
  alert(i);  // alert boxes showing 4, 3, 2, 1, 0
}

iTek ifadede hem karşılaştırma hem de azaltma yapılabilir.

Bunun neden daha az x86 yönergesine indirgendiğini öğrenmek için diğer yanıtlara bakın.

Uygulamanızda anlamlı bir fark yaratıp yaratmadığına gelince, sanırım bu kaç döngüye sahip olduğunuza ve bunların ne kadar derinlemesine iç içe olduklarına bağlıdır. Ama bana göre, bu şekilde yapmak da aynı şekilde okunabilir, bu yüzden yine de yapıyorum.


Bunun zayıf bir stil olduğunu düşünüyorum, çünkü okuyucunun bir döngüyü kaydetmenin olası değeri için i'nin dönüş değerinin i'nin eski değeri olduğunu bilmesine bağlı. Bu, yalnızca çok sayıda döngü yinelemesi olsaydı ve döngü yineleme uzunluğunun önemli bir kısmıysa ve aslında çalışma zamanında ortaya çıktıysa önemli olurdu. Sonra, birisi (i = 5; --i;) için deneyecek çünkü C ++ 'da i önemsiz olmayan bir türken geçici oluşturmaktan kaçınmak isteyebileceğinizi duymuşlar ve şimdi böcek ülkesindesiniz. Yanlış kodun yanlış görünmesi fırsatını acımasızca çöpe at.
mabraham

0

Şimdi, yeterince montaj dersiniz olduğunu düşünüyorum :) Size yukarıdan aşağıya yaklaşımı için başka bir neden sunmak istiyorum.

Yukarıdan gitme sebebi çok basit. Döngünün gövdesinde, yanlışlıkla sınırı değiştirebilir, bu da yanlış davranışa veya hatta sonlanmayan döngüye neden olabilir.

Java kodunun bu küçük kısmına bir bakın (bu nedenle dilin önemi yok sanırım):

    System.out.println("top->down");
    int n = 999;
    for (int i = n; i >= 0; i--) {
        n++;
        System.out.println("i = " + i + "\t n = " + n);
    }
    System.out.println("bottom->up");
    n = 1;
    for (int i = 0; i < n; i++) {
        n++;
        System.out.println("i = " + i + "\t n = " + n);
    }

Demek istediğim, yukarıdan aşağıya gitmeyi veya sınır olarak bir sabiti tercih etmeyi düşünmelisiniz.


Ha? !! Başarısız olan örnek gerçekten sezgisel değil, yani bir saman adam argümanı - bunu kimse yazmaz. Biri yazardı for (int i=0; i < 999; i++) {.
Lawrence Dol

@Yazılım Maymunu, bazı hesaplamaların bir sonucu olduğunu hayal eder ... örneğin, bazı koleksiyonlar üzerinde yineleme yapmak isteyebilirsiniz ve boyutu sınırdır, ancak bazı yan etkiler olarak, döngü gövdesinde koleksiyona yeni öğeler eklersiniz.
Gabriel Ščerbák

İletmek istediğiniz şey buysa, o zaman örneğiniz bunu göstermelidir:for(int xa=0; xa<collection.size(); xa++) { collection.add(SomeObject); ... }
Lawrence Dol

@Yazılım Maymun Sadece koleksiyonlar hakkında konuşmaktan daha genel olmak istedim çünkü akıl
yürüttüğüm şeyin

2
Evet, ancak örnek yoluyla akıl yürütecekseniz, örneklerinizin inandırıcı ve konuyu açıklayıcı olması gerekir.
Lawrence Dol

-1

Bir derleyici düzeyinde, sıfıra doğru geri sayan bir döngü, genellikle belirli bir değere kadar sayılan döngüden biraz daha hızlıdır. Bir hesaplamanın sonucu sıfıra eşitse, çoğu işlemci bir sıfır bayrağı ayarlayacaktır. Biri çıkarmak sıfırın etrafında bir hesaplama sarmalı yapıyorsa, bu normalde taşıma işaretini değiştirecektir (bazı işlemcilerde bunu diğerlerinde ayarlayacaktır, bu onu temizleyecektir), bu nedenle sıfır ile karşılaştırma esasen ücretsizdir.

Yineleme sayısı sabit değil de değişken olduğunda bu daha da doğrudur.

Önemsiz durumlarda derleyici bir döngünün sayma yönünü otomatik olarak optimize edebilir, ancak daha karmaşık durumlarda programcı döngünün yönünün genel davranışla alakasız olduğunu biliyor olabilir, ancak derleyici bunu kanıtlayamaz.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.