C'deki kayma ve çarpma arasındaki zaman farkını test ettiğimde fark yok. Niye ya?


28

İkili olarak kaydırmanın 2 ^ k ile çarpmaktan çok daha etkili olduğunu öğrendim. Bu yüzden denemek istedim ve bunu test etmek için aşağıdaki kodu kullandım:

#include <time.h>
#include <stdio.h>

int main() {
    clock_t launch = clock();
    int test = 0x01;
    int runs;

    //simple loop that oscillates between int 1 and int 2
    for (runs = 0; runs < 100000000; runs++) {


    // I first compiled + ran it a few times with this:
    test *= 2;

    // then I recompiled + ran it a few times with:
    test <<= 1;

    // set back to 1 each time
    test >>= 1;
    }

    clock_t done = clock();
    double diff = (done - launch);
    printf("%f\n",diff);
}

Her iki versiyon için de çıktı yaklaşık 440000'dü, 10000 veriyor ya da alıyordu. İki versiyonun çıktıları arasında (görsel olarak, en azından) anlamlı bir fark yoktu. Öyleyse sorum şu, metodolojimde yanlış bir şeyler mi var? Görsel bir fark bile olmalı mı? Bunun bilgisayarımın, derleyicinin veya başka bir şeyin mimarisiyle ilgisi var mı?


47
Sana kim öğretti açık bir şekilde yanlıştı. Bu inanç, 1970'lerden bu yana, tipik olarak kullanılan mimarilerde tipik olarak kullanılan derleyiciler için geçerli değildi. Bu iddiayı test ettiğiniz için iyi. Cennetin uğruna JavaScript hakkında yapılan bu saçma iddia olduğunu duydum .
Eric Lippert,

21
Bu gibi soruları cevaplamanın en iyi yolu, derleyicinin ürettiği montaj koduna bakmaktır. Derleyiciler tipik olarak oluşturdukları montaj dilin bir kopyasını üretme seçeneğine sahiptir. GNU GCC derleyicileri için bu '-S' dir.
Charles E. Grant,

8
Bir bu baktıktan sonra işaret olmalıdır gcc -S, kod test *= 2aslında derlenmiş shll $1, %eax ile çağrıldığında gcc -O3 -Sbile bir döngü yok. İki saat görüşmesi ayrı bir hat var:callq _clock movq %rax, %rbx callq _clock

6
“İkili olarak kaydırmanın 2 ^ k ile çarpmaktan çok daha etkili olduğunu öğrendim”; yanlış olduğu ortaya çıkan (veya en azından güncel olmayan) birçok şey öğrenilir. Smartish bir derleyici her ikisi için de aynı vardiya işlemini kullanacaktır.
John Bode

9
Bu tür bir optimizasyon üzerinde çalışırken, ölçtüğünüzü düşündüğünüzü ölçtüğünüzden emin olmak için her zaman oluşturulan montaj kodunu kontrol edin. Çok fazla sayıda "neden bu zamanları görüyorum" sorusu SO'nun derleyiciye kaynaştığını ve sonuçları kullanılmadığı için işlemleri tamamen ortadan kaldırdığını gösteriyor.
Russell Borogove,

Yanıtlar:


44

Diğer cevapta belirtildiği gibi, çoğu derleyici otomatik olarak bit değişimleriyle yapılacak çarpımları optimize edecektir.

Bu, optimizasyon yaparken çok genel bir kuraldır: Çoğu “optimizasyon”, aslında derleme ile ilgili ne demek istediğinizi yanlış yönlendirir ve performansı daha da düşürebilir.

Yalnızca bir performans sorunu fark ettiğinizde ve sorunun ne olduğunu ölçtüğünüzde optimize edin. (ve yazdığımız çoğu kod o kadar sık ​​çalıştırılmaz, bu yüzden rahatsız etmemize gerek yoktur)

Optimize etmenin en büyük dezavantajı 'optimize edilmiş' kodun genellikle daha az okunabilir olmasıdır. Öyleyse, sizin durumunuzda, çoğaltmak istediğinizde daima çarpma işlemine gidin. Ve bitleri hareket ettirmek istediğinizde biraz kaydırma yapmak için gidin.


20
Her zaman semantik olarak doğru olan işlemi kullanın. Eğer bit maskelerini değiştiriyorsanız veya küçük tamsayıları daha büyük tamsayılara yerleştiriyorsanız, kaydırma uygun işlemdir.
ddyer

2
Üst düzey bir yazılım uygulamasında hiç (pratik olarak konuşursak) bir çarpma operatörünü çarpma işlemine optimize etme ihtiyacı doğar mıydı? Derleyici zaten en iyi duruma getirdiği için, bu bilgiyi elde etmenin tek faydası çok düşük bir seviyede programlama yapmaktır (en azından derleyicinin altında).
NicholasFolk,

11
@NicholasFolk hayır. Anlamak en basit olanı yapın. Derlemeyi doğrudan yazıyorsanız yararlı olabilir ... ya da bir optimizasyon derleyicisi yazıyorsanız, yine de yararlı olabilir. Ancak bu iki durumun dışında, ne yaptığınızı gizleyen ve bir sonraki programcı yapan ( nerede oturduğunuzu bilen bir balta cinayeti olan ) bir hiledir, adınızı lanetler ve bir hobi edinmeyi düşünür.

2
@NicholasFolk: Bu seviyedeki optimizasyonlar zaten CPU mimarisi tarafından neredeyse her zaman gizlenir veya işlenir. Sadece argümanları bellekten alırken ve geri yazarken 50 döngü kurtarıp kaydetmemeniz kimin umurunda 100'den fazla sürer? Bunun gibi mikro-optimizasyonlar bellek CPU hızına ulaştığında (veya bu hıza yaklaştığında) anlamlıdır, ancak bugün çok fazla değildir.
TMN

2
Çünkü bu teklifin% 10'unu görmekten bıktım ve buradaki kafayı çivileştirdiği için: "Verimlilik kepçesinin kötüye kullanılmasına neden olduğuna dair hiçbir şüphe yok. ayıklama ve bakım göz önüne alındığında yaklaşık, kendi programlarının kritik olmayan kısımlarının hız ve verimlilikte bu girişimler aslında güçlü bir negatif etkiye sahip Biz. etmelidir zamanın% 97 hakkında söylenecek, küçük verimlilik unutun: prematüre optimizasyonu köküdür tüm kötülükler ... ...
cHao

25

Derleyici sabitleri tanır ve çarpımları uygun olan yer değiştirmeye dönüştürür.


Derleyici, 2… 'nin gücü olan sabitleri tanır ve vardiyaya dönüştürür. Tüm sabitler vardiya halinde değiştirilemez.
hızla_

4
@quickly_now: Vardiya ve toplama / çıkarma kombinasyonlarına dönüştürülebilirler.
Mehrdad,

2
Klasik bir derleyici eniyileyici hatası, bölmeleri pozitif temettüler için işe yarayan ancak negatif için 1'e kadar olan sağa kaymalara dönüştürmektir.
ddyer

1
@quickly_now 'Uygun olan' teriminin, bazı sabitlerin vardiya olarak yeniden yazılamadığı fikrini kapsadığına inanıyorum.
Pharap

21

Kaymanın çarpmadan daha hızlı olup olmadığı işlemcinizin mimarisine bağlıdır. Pentium ve daha önceki günlerde, kaydırma, çoğaltmanızdaki 1 bit sayısına bağlı olarak, çarpma işleminden genellikle daha hızlıydı. Örneğin, eğer çarpımınız 320 ise, bu 101000000, iki bit.

a *= 320;               // Slower
a = (a<<7) + (a<<9);    // Faster

Ama ikiden fazla bit olsaydı ...

a *= 324;                        // About same speed
a = (a<<2) + (a<<7) + (a<<9);    // About same speed

a *= 340;                                 // Faster
a = (a<<2) + (a<<4) + (a<<7) + (a<<9);    // Slower

Tek döngülü bir PIC18 gibi küçük bir mikrodenetleyicide çarpın, ancak namlu kaydırıcı yok , eğer 1 bitden daha fazla kayıyorsanız çarpma daha hızlı olur.

a  *= 2;   // Exactly the same speed
a <<= 1;   // Exactly the same speed

a  *= 4;   // Faster
a <<= 2;   // Slower

Eski Intel işlemcilerinde doğru olanların tam tersi olduğuna dikkat edin .

Ama yine de o kadar basit değil. Superscalar mimarisi nedeniyle doğru hatırlıyorsam, Pentium aynı anda bir çarpma talimatını ya da iki vardiya komutunu aynı anda işleyebildi (birbirlerine bağlı olmadıkları sürece). Bu, iki değişkeni 2 ile çarpmak isterseniz , o zaman kaymanın daha iyi olabileceği anlamına gelir.

a  *= 4;   // 
b  *= 4;   // 

a <<= 2;   // Both lines execute in a single cycle
b <<= 2;   // 

5
+1 "Kaymanın çarpma işleminden daha hızlı olup olmadığı işlemcinizin mimarisine bağlıdır." Aslında tarihe biraz girdiğiniz ve çoğu bilgisayar efsanesinin mantıklı bir temeli olduğunu gösterdiğiniz için teşekkür ederiz.
Pharap

11

Test programınızla ilgili birkaç probleminiz var.

İlk olarak, aslında değerini kullanmıyorsunuz test. C standardının içinde, değerin testönemli olmasının imkânı yok. Doktoru bu tamamen ücretsizdir. Onu çıkardıktan sonra, döngü aslında boş. Görülebilir tek etki ayarlamaktır runs = 100000000, ancak runskullanılmaz. Böylece optimizer tüm döngüyü kaldırabilir (ve gerekir!). Kolay düzeltme: hesaplanan değeri de yazdırın. Yeterince belirlenmiş bir optimizasyon cihazının hala döngüyü uzaklaştırabileceğini unutmayın (derleme zamanında bilinen tamamen sabitlere dayanır).

İkincisi, birbirinizi iptal eden iki işlem yaparsınız. İyileştiricinin bunu fark etmesine ve iptal etmesine izin verilir . Yine boş bir döngü bırakarak ve kaldırıldı. Bunu düzeltmek zor düpedüz zordur. Bir unsigned intşeye geçebilirsiniz (taşma tanımsız davranış değildir), ancak bu elbette sadece 0 ile sonuçlanır test += 1.

Son olarak, test *= 2bunun çarpıma derleneceğini varsayıyorsunuz . Bu çok basit bir optimizasyon; eğer bitshift daha hızlı ise, optimizer bunun yerine kullanır. Bunun üstesinden gelmek için, uygulamaya özel bir montaj satır içi gibi bir şey kullanmanız gerekir.

Ya da sanırım hangisinin daha hızlı olduğunu görmek için mikroişlemci veri sayfanızı kontrol edin.

Programınızı gcc -S -O34.9 sürümünü kullanarak derlemenin derleme çıktısını kontrol ettiğimde , optimizer aslında her basit varyasyonu ve daha fazlasını gördü. Her durumda, döngüyü kaldırdı (sabit atama test), geriye kalan tek şey clock(), convert / subtract ve printf.


1
Ayrıca, optimize edicinin, sabitleyicilerdeki (bir döngüde bile) işlemleri, sqrt c # vs sqrt c ++ ' da gösterildiği gibi , optimizer'ın gerçek toplam ile bir değeri toplayan bir döngüyü değiştirebildiği yerlerde , optimizasyon yapabileceğini (ve yapacağını) unutmayın . Bu optimizasyonu yenmek için çalışma zamanında belirlenen bir komut kullanmanız gerekir (komut satırı argümanı gibi).

@MichaelT Yep. Demek istediğim, "Yeterince kararlı bir optimizasyon cihazının hala döngüyü optimize edebileceğini unutmayın (derleme zamanında bilinen sabitlere dayanır)."
derobert

Ne dediğini anlıyorum ama derleyicinin tüm döngüyü kaldırdığını sanmıyorum. İterasyon sayısını basitçe artırarak bu teoriyi kolayca test edebilirsiniz. Yinelemeleri artırmanın programın daha uzun sürmesini sağladığını göreceksiniz. Döngü tamamen kaldırılmış olsaydı bu durum böyle olmazdı.
DollarAkshay

@AkshayLAradhya Ne diyemeyiz sizin derleyici yapıyor, ama o yine doğruladı gcc -O3(şimdi 7.3'e) hala tamamen döngü kaldırır. (Gerekirse int yerine uzun sürdüğünüzden emin olun, aksi halde taşma nedeniyle sonsuz bir döngüye dönüştürür).
derobert

8

Sanırım soru sahibinin daha farklı bir cevabı bulmasının daha yararlı olacağını düşünüyorum, çünkü sorularda ve bazı cevaplarda veya yorumlarda incelenmemiş bazı varsayımlar görüyorum.

Sonuçta ortaya çıkan göreceli kaydırma ve çarpma çalışma süresinin C ile hiçbir ilgisi yoktur. C derken, bunun ya da GCC'nin o sürümü gibi belirli bir uygulamanın örneği değil, dilin anlamına gelmez. Bu reklamı saçmalamak istemem, ama örnekleme için aşırı bir örnek kullanmak isterim: tamamen standartlara uygun bir C derleyicisi uygulayabilir ve çarpma bir milisaniyeyi veya diğer yollardan giderken bir saat sürebilir. C veya C ++ ile bu tür performans kısıtlamaları farkında değilim.

Tartışmada bu teknikliği önemsemezsiniz. Amacınız muhtemelen çarpmalara karşı vardiya yapmanın göreceli performansını test etmekti ve C'yi seçtiniz, çünkü genellikle düşük seviyeli bir programlama dili olarak algılanıyor, bu nedenle kaynak kodunun doğrudan ilgili talimatlara daha doğrudan çevrilmesini bekleyebilirsiniz. Bu tür sorular çok yaygındır ve bence iyi bir cevap, C kodunda bile kaynak kodunuzun verilen bir durumda düşündüğünüz gibi doğrudan talimatlara çevrilmediğine işaret etmelidir. Aşağıda size bazı olası derleme sonuçları verdim.

Gerçek dünyadaki yazılımda bu eşdeğeri yerine kullanmanın yararını sorgulayan yorumlar burada gelir. Eric Lippert'den olduğu gibi, sorunuzun yorumlarında bazılarını görebilirsiniz. Bu tür optimizasyonlara cevap olarak genellikle daha tecrübeli mühendislerden alacağınız reaksiyonla uyumludur. Üretim kodunda ikili vardiyayı çarpma ve bölme aracı olarak kullanırsanız, insanlar kodunuzda büyük olasılıkla kodlar ve bir dereceye kadar duygusal tepkiler yaşarlar ("Cennetin uğruna JavaScript ile ilgili bu saçma iddiayı duydum.") Bu tepkilerin nedenlerini daha iyi anlamadıkça, programcıları yeni başlayanlar için mantıklı gelmeyebilir.

Bu nedenler, esasen, göreceli performanslarını karşılaştırırken bulmuş olabileceğiniz için, bu tür optimizasyonların okunabilirliği ve düşüklüğünün bir birleşimidir. Ancak, eğer çarpma için vardiya değişimi bu tür optimizasyonların tek örneği ise, insanların bir tepkiden daha güçlü olacağını düşünmüyorum. Sizinki gibi sorular sıklıkla çeşitli biçimlerde ve çeşitli bağlamlarda ortaya çıkar. Sanırım daha kıdemli mühendislerin gerçekte bu kadar güçlü tepki göstermesinin, en azından zaman zaman sahip olduğum gibi, insanlar kod bazında bu tür mikro optimizasyonları liberal bir şekilde kullandıklarında daha geniş bir zarar aralığı potansiyeli olduğunu düşünüyorum. Microsoft gibi bir şirkette büyük bir kod tabanında çalışıyorsanız, diğer mühendislerin kaynak kodlarını okumak veya burada belirli bir kodu bulmaya çalışmak için çok zaman harcarsınız. Hatta birkaç yıl sonra, özellikle çağrı yapmadan yapılan bir aramayı takiben bir üretim kesintisini düzeltmek zorunda kaldığınızda olduğu gibi, en uygun olmayan zamanlarda, bir şeyler ifade etmeye çalışacağınız kendi kodunuz bile olabilir. Cuma gecesi görev, arkadaşlarla eğlenmek için bir geceye çıkmak üzere… Kod okumaya çok zaman harcıyorsanız, mümkün olduğu kadar okunaklı olduğunu takdir edeceksiniz. En sevdiğiniz romanı okuduğunuzu hayal edin, ancak yayıncı, abbrv kullandıkları yeni bir sürüm yayınlamaya karar verdi. tüm ov th pl bc thc thnk ss spc. Bu, diğer mühendislerin kodunuza verebilecekleri tepkilere benzer; bu tür optimizasyonlarla onları serpiştirirseniz. Diğer cevapların da belirttiği gibi, ne demek istediğinizi açıkça belirtmek daha iyidir.

Bu ortamlarda bile, kendinizi veya başka bir denkliği bilmeniz beklenen bir röportaj sorusu çözerken kendinizi bulabilirsiniz. Bunları bilmek kötü değildir ve iyi bir mühendis, ikili kaymanın aritmetik etkisinin farkında olacaktır. Bunun bence iyi bir mühendis olduğunu söylemediğimi, ancak bence iyi bir mühendisin bileceğini unutmayın. Özellikle de, bu akıllı mühendislik "hilesini" kodlama sorusunda size göstermeyi ve kendisinin kanıtladığını kanıtlamaktan memnuniyet duyacağınızı umarak, genellikle görüşme döngüsünüzün sonuna doğru, bazı yöneticiler bulabilirsiniz. Ayrıca, eskiden beri "yönetici" değil, anlayışlı mühendislerden biriydi. Bu gibi durumlarda, etkilenmiş görünmeye çalışın ve aydınlatıcı röportaj için ona teşekkür edin.

C de neden bir hız farkı görmediniz? En muhtemel cevap, her ikisinin de aynı montaj koduyla sonuçlanmış olmasıdır:

int shift(int i) { return i << 2; }
int multiply(int i) { return i * 2; }

Her ikisi de içine derleyebilir

shift(int):
    lea eax, [0+rdi*4]
    ret

Optimizasyonları olmayan GCC'de, örneğin "-O0" bayrağını kullanarak, şunu alabilirsiniz:

shift(int):
    push    rbp
    mov rbp, rsp
    mov DWORD PTR [rbp-4], edi
    mov eax, DWORD PTR [rbp-4]
    sal eax, 2
    pop rbp
    ret
multiply(int):
    push    rbp
    mov rbp, rsp
    mov DWORD PTR [rbp-4], edi
    mov eax, DWORD PTR [rbp-4]
    add eax, eax
    pop rbp
    ret

Gördüğünüz gibi, "-O0" u GCC’ye geçirmek, ne tür bir kod ürettiği hakkında biraz akıllıca olacağı anlamına gelmez. Özellikle, bu durumda bile derleyicinin bir çarpma talimatı kullanmaktan kaçındığına dikkat edin. Aynı denemeyi başka sayılarla ve hatta iki sayının gücü olmayan sayılarla çarpmalarla tekrarlayabilirsiniz. Muhtemelen platformunuzda bir kayma ve ekleme kombinasyonu göreceksiniz, ancak çarpma yok. Eğer çarpma ve kaymalar gerçekten aynı maliyete sahipse, derleyicinin görünüşte çarpımları kullanmaktan kaçınması biraz tesadüf gibi görünüyor, değil mi? Ancak ispat için varsayım sağlamak istemem, o yüzden devam edelim.

Testinizi yukarıdaki kodla tekrar başlatabilir ve şimdi bir hız farkı görüp görmediğinizi görebilirsiniz. O zaman bile, çarpma ile çarpma testini test etmiyorsunuz, çarpma yokluğunu görerek görüyorsunuz, ancak GCC tarafından kayma C işlemleri için belirli bayrak dizileriyle oluşturulan ve belirli bir örnekte çarpma kodu . Böylece başka bir testte montaj kodunu elle düzenleyebilir ve bunun yerine "çarpma" yönteminin kodunda "imul" komutunu kullanabilirsiniz.

Derleyicinin bu akıllılarından bazılarını yenmek istersen, daha genel bir kayma ve çarpma yöntemi tanımlayabilir ve bunun gibi bir şeyle sonuçlanabilir:

int shift(int i, int j) { return i << j; }
int multiply(int i, int j) { return i * j; }

Aşağıdaki montaj kodunu verebilecek olan:

shift(int, int):
    mov eax, edi
    mov ecx, esi
    sal eax, cl
    ret
multiply(int, int):
    mov eax, edi
    imul    eax, esi
    ret

En sonunda, GCC 4.9'un en iyi optimizasyon seviyesinde bile, başlangıçta testinize başlarken beklediğiniz montaj talimatlarındaki ifadeye sahibiz. Performans optimizasyonunda kendi başına önemli bir ders olabileceğini düşünüyorum. Derleyicinin uygulayabileceği akıllılar açısından, kodumuzda somut sabitlerin yerine değişkenler koymak için yarattığı farkı görebiliriz. Kaydırmalı çarpma sübstitüsyonu gibi mikro optimizasyonlar, bir derleyicinin genellikle kendi başına kolayca yapabileceği çok düşük seviyeli optimizasyonlardır. Performans üzerinde çok daha etkili olan diğer optimizasyonlar , kodun amacının anlaşılmasını gerektirirBu derleyici tarafından genellikle erişilebilir değildir veya sadece bazı sezgiseller tarafından tahmin edilebilir. Bir yazılım mühendisi olarak buraya girersiniz ve kesinlikle tipik olarak çarpmaların çarpımlarını değiştirmeyi içermez. G / Ç üreten ve bir işlemi engelleyebilen bir hizmete fazladan bir çağrı yapılmaması gibi faktörleri içerir. Sabit diskinize ya da tanrınız, zaten bellekte olanlardan elde edebileceğiniz bazı ekstra veriler için uzaktaki bir veritabanına girmezseniz, beklediğiniz süre milyonlarca talimatın yerine getirilmesinden ağır basar. Şimdi, sanırım asıl sorunuzdan biraz sapmışız, ancak bunu bir soru sormaya işaret etmeyi düşünüyorum, özellikle de kodun tercümesi ve yürütülmesi hakkında yeni bir şeyler öğrenmeye başlıyorsak,

Peki hangisi daha hızlı olacak? Performans farkını gerçekten test etmeyi seçmenin iyi bir yaklaşım olduğunu düşünüyorum. Genel olarak, bazı kod değişikliklerinin çalışma zamanı performansından şaşırmak kolaydır. Modern işlemcilerin kullandığı birçok teknik vardır ve yazılımlar arasındaki etkileşim de karmaşık olabilir. Bir durumda belirli bir değişiklik için faydalı performans sonuçları alsanız bile, bu tür bir değişikliğin her zaman performans yararları sağlayacağı sonucuna varmanın tehlikeli olduğunu düşünüyorum. Bir kez böyle bir test yapmanın tehlikeli olduğunu düşünüyorum, "Tamam, şimdi hangisinin daha hızlı olduğunu biliyorum!" ve ardından, ölçümlerinizi tekrarlamadan, aynı optimizasyonu üretim koduna ayırt etmeden uygulayın.

Peki, vardiya çarpma işleminden daha hızlıysa ne olur? Bunun neden doğru olacağına dair kesinlikle belirtiler var. GCC, yukarıda gördüğünüz gibi, diğer talimatların lehine doğrudan çarpmadan kaçınmanın iyi bir fikir olduğunu düşünüyor (optimizasyon olmadan bile). Intel 64 ve IA-32 Mimarileri Optimizasyon Referans Kılavuzu size CPU talimatları nispi maliyeti hakkında fikir verecektir. Daha fazla gecikme ve verime odaklanan başka bir kaynak ise http://www.agner.org/optimize/instruction_tables.pdf. Bunların mutlak bir çalışma zamanı için iyi bir tahminci değil, birbirlerine göre talimatların performansının bir göstergesi olduğunu unutmayın. Sıkı bir döngüde, testiniz simüle edilirken, "verim" metriği en alakalı olmalıdır. Belirli bir talimatın yürütülmesi için bir yürütme biriminin tipik olarak bağlanacağı döngü sayısıdır.

Peki, vardiya çarpma işleminden daha hızlı değilse ne olur? Yukarıda söylediğim gibi, modern mimariler oldukça karmaşık olabilir ve dal tahmini, önbellekleme, boru hattı ve paralel yürütme birimleri gibi şeyler zaman zaman mantıksal olarak eşdeğer iki kod parçasının göreceli performansını tahmin etmeyi zorlaştırabilir. Bunu gerçekten vurgulamak istiyorum, çünkü bu, bunun gibi soruların cevaplarının çoğundan memnun olmadığım yer ve buradaki insanların kampının tamamen doğru olmadığını (artık) değiştirmenin çarpma işleminden daha hızlı olduğunu söylüyor.

Hayır, farkında olduğum kadarıyla 1970'lerde bazı gizli mühendislik sosu icat etmedik ya da ne zaman bir çarpma biriminin ve biraz değiştiricinin maliyet farkını aniden iptal edemedik. Mantıksal kapılar açısından ve kesinlikle mantıksal işlemler açısından genel bir çarpma, birçok senaryoda, birçok mimaride varil kaydırmalı bir kaymadan daha karmaşıktır. Bunun bir masaüstü bilgisayardaki genel çalışma zamanına nasıl çevrileceği biraz opak olabilir. Belirli işlemcilere nasıl uygulandıklarını tam olarak bilmiyorum, ancak işte çarpımın açıklaması: Tam sayı çarpımı, modern CPU'ya eklenmesiyle aynı hızda mı?

Burada bir Namlu Değiştiren açıklamasıdır . Önceki paragrafta atıfta bulunduğum belgeler CPU işlem proxy'si tarafından işlemlerin göreceli maliyeti hakkında başka bir görünüm sunar. Intel'deki mühendisler sık ​​sık benzer sorular alıyor gibi görünüyor: intel geliştirici bölgesi , tamsayılı çarpma ve çekirdek 2 ikili işlemciye ekleme için saat döngüleri sunuyor

Evet, çoğu gerçek hayat senaryosunda ve neredeyse kesinlikle JavaScript’te performansın uğruna bu eşdeğerliği kullanmaya çalışmak muhtemelen boşuna bir girişimdir. Bununla birlikte, çarpma talimatlarını kullanmaya zorladıysak ve daha sonra çalışma süresinde hiçbir fark görmemiş olsak bile, bu, daha önce kullandığımız maliyet ölçümünün niteliği nedeniyle, kesin olmaktır, maliyet farkı olmadığı için değildir. Uçtan uca çalışma zamanı bir metriktir ve tek umursadığımız tek şeyse her şey yolunda. Ancak bu çarpma ve kayma arasındaki tüm maliyet farklılıklarının ortadan kalktığı anlamına gelmez. Ve bence bu fikri, sorgulayıcıya, ima yoluyla veya başka şekilde, modern kodun çalışma zamanına ve maliyetine dahil olan faktörler hakkında bir fikir edinmeye başlayan, kesinlikle doğru bir fikir değil. Mühendislik her zaman değişimlerle ilgilidir. Modern işlemcilerin, gördükleri kullanıcılar olarak gördüklerimizi yürütme zamanını göstermek için yaptıkları tradeollüklere ilişkin soruşturma ve açıklama, daha farklı bir cevap verebilir. Ve daha az mühendisin mikro-optimize edilmiş kodun okunabilirliğini ortadan kaldırarak denetlediğini görmek istiyorsak, "bu artık doğru değil" den daha farklı bir cevabın garanti edildiğini düşünüyorum çünkü bu tür "optimizasyonların" doğasını daha genel bir şekilde anlamayı gerektirir çeşitli, eski enkarnasyonlarını, güncel olmayan bazı özel örneklere atıfta bulunmaktan çok saptamak.


6

Gördüğün şey, iyimserin etkisi.

İyileştiricilerin görevi, derlenen kodu daha küçük veya daha hızlı hale getirmektir (ancak aynı anda nadiren her ikisi de ... ancak pek çok şey gibi ... kodun ne olduğuna dair DEPENDS).

PRENSİP’de, bir çarpma kütüphanesine yapılan herhangi bir çağrı veya sık sık bir donanım çarpanının kullanılması bile, sadece bit yönünde kaydırma yapmaktan daha yavaştır.

Öyleyse ... eğer saf derleyici * 2 operasyonu için bir kütüphaneye çağrı yaptıysa, o zaman elbette bitsel kaymadan * daha yavaş çalışacaktır.

Ancak, iyimserler, kalıpları tespit etmek ve kodun nasıl daha küçük / daha hızlı / ne olursa olsun nasıl yapılacağını bulmak için vardır. Ve gördüğünüz şey, derleyicinin * 2'nin bir vardiya ile aynı olduğunu tespit etmesidir.

Tıpkı bir ilgi alanı olarak, bugün sadece * 5 ... gibi bazı işlemler için üretilen montajcıya bakıyordum, aslında buna bakmadım ama başka şeyler ve derleyicinin * 5'e dönüştüğünü fark ettiğimde:

  • vardiya
  • vardiya
  • orijinal numara ekle

Bu nedenle, derleyicimin optimizeri genel amaçlı bir çarpma kütüphanesine yapılan çağrılar yerine satır içi kaymalar üretecek ve ekleyebilecek kadar akıllıydı (en azından belirli küçük sabitler için).

Derleyici optimisers sanatı, büyü ile dolu ve tüm gezegende yaklaşık 6 kişi tarafından tam olarak anlaşılan, tamamen ayrı bir konudur :)


3

Şununla zamanlamayı dene:

for (runs = 0; runs < 100000000; runs++) {
      ;
}

Derleyici test, döngünün her yinelemesinden sonra değerinin değişmediğini ve son değerinin testkullanılmadığını ve döngünün tamamen ortadan kaldırıldığını kabul etmelidir .


2

Çarpma, kaymalar ve eklemelerin bir birleşimidir.

Bahsettiğiniz durumda, derleyicinin onu optimize edip etmemesi önemli değil, " xiki ile çarp" ifadesinin her ikisi de uygulanabilir:

  • xBir yerin bit parçalarını sola kaydırın .
  • Ekle xiçin x.

Bunların her biri temel atomik işlemlerdir; biri diğerinden daha hızlı değil.

"Dörtle çarp x", (veya herhangi biri 2^k, k>1) olarak değiştirin ve biraz farklı:

  • xİki yerden sola kaydırın .
  • Ekle xiçin xve diyoruz yeklemek yiçin y.

Temel bir mimariye günü, 's basit vardiya daha verimli olduğunu görmek - Bir ve iki alma işlemleri, biz ekleyemezsiniz beri yhiç ybiz ne olduğunu bilmeden yolduğunu.

İkinciyi (veya herhangi birini 2^k, k>1), uygulamada aynı şey olacak şekilde optimize etmenizi önlemek için uygun seçeneklerle deneyin . Kaydırma O(1)işleminin, içinde tekrarlanan eklemeye göre daha hızlı olduğunu bulmalısınız O(k).

Açıkça, çoklu çarpma iki gücün olmadığı durumlarda, bir kayma ve ekleme (her birinin sayısının sıfır olmadığı bir) birleşimine ihtiyaç vardır.


1
"Temel atomik işlem" nedir? Bir vardiyada, işlemin her bit için paralel olarak uygulanabileceğini iddia edemezken, en soldaki bitlerin diğer bitlere bağlı olduğunu iddia edemez miyiz?
Bergi

2
@Bergi: Sanırım hem vardiya hem de ekleme işlemlerinin tek bir makine talimatı olduğunu söylüyor. Her birinin döngü sayımını görmek için komut seti belgelerine bakmanız gerekir, ancak evet, bir eklenti genellikle bir çoklu döngü işlemidir, oysa bir değişim genellikle tek bir döngüde gerçekleştirilir.
TMN

Evet, durum böyle olabilir, ama çarpma tek bir makine talimatıdır (tabii ki daha fazla devir gerektirebilir)
Bergi

@ Bergi, bu da kemer bağımlı. Hangi kemerin 32 bit ekleme (veya uygunsa x-bit) işleminden daha az döngüde değiştiğini düşünüyorsunuz?
OJFord

Belirli bir mimariyi bilmiyorum, hayır (ve bilgisayar mühendisliği kurslarım kayboldu), muhtemelen her iki komut da bir döngüden daha az sürer. Muhtemelen bir kaymanın daha ucuz olacağını düşündüğüm mikro kod ve hatta mantık kapıları hakkında düşünüyordum.
Bergi

1

İmzalanmış veya işaretsiz değerlerin ikisinin güçleri ile çarpılması sola kaymaya eşdeğerdir ve çoğu derleyici yerine geçecektir. İmzasız değerlerin bölünmesi veya derleyicinin asla olumsuz olamayacağı imzalı değerler , sağa kaymaya eşdeğerdir ve çoğu derleyici bu ikame işlemini yapacaktır (bazıları imzalı değerler negatif olamazken kanıtlayacak kadar karmaşık olmamakla birlikte) .

Ancak, potansiyel olarak negatif işaretli değerlerin bölünmesinin sağa kaymaya eşdeğer olmadığı belirtilmelidir . Gibi bir ifade (x+8)>>4eşdeğer değildir (x+8)/16. Eski, derleyicilerin% 99'unda -24 ila -9 ila -1, -8 ila +7 ila 0 ve +8 ila +23 ila 1 [neredeyse sıfir simetrik olarak yuvarlama sayıları] değerlerini eşler. Sonuncusu -39 ila -24 ila -1, -23 ila +7 ila 0 ve +8 ila +23 ila +1 (genel olarak asimetrik ve büyük olasılıkla amaçlananları değil) eşleyecektir. Değerlerin negatif olması beklenmese bile , derleyicinin değerlerin negatif olamayacağını kanıtlayamadıklarından >>4daha hızlı bir kod /16kullanılacağını unutmayın.


0

Biraz daha bilgi kontrol ettim.

X86_64'de, MUL opcode 10 çevrim gecikme süresi ve 1/2 çevrim verimine sahiptir. MOV, ADD ve SHL 1 çevrim gecikme süresi, 2,5, 2,5 ve 1,7 çevrim verimine sahiptir.

15 ile çarpma, en az 3 SHL ve 3 ADD op ve muhtemelen birkaç MOV gerektirir.

https://gmplib.org/~tege/x86-timing.pdf


0

Metodolojiniz hatalı. Döngü artışınız ve durum kontrolünüzün kendisi bu kadar zaman alıyor.

  • Boş bir döngü çalıştırmayı deneyin ve süreyi ölçün (arayın base).
  • Şimdi 1 vardiya işlemi ekleyin ve süreyi ölçün (arayın s1).
  • Sonra 10 vardiya işlemi ekleyin ve süreyi ölçün (arayın s2)

Her şey doğru gidiyorsa, base-s210 kat daha fazla olmalıdır base-s1. Aksi halde, burada başka bir şey devreye giriyor.

Şimdi bunu gerçekten kendim denedim ve düşündüm. Eğer döngüler bir soruna neden oluyorsa, neden tamamen çıkarmıyorlar? Ben de devam ettim ve bunu yaptım:

int main(){

    int test = 2;
    clock_t launch = clock();

    test << 6;
    test << 6;
    test << 6;
    test << 6;
    //.... 1 million times
    test << 6;

    clock_t done = clock();
    printf("Time taken : %d\n", done - launch);
    return 0;
}

Ve işte sonucun var.

1 milisaniyede 1 milyon vardiya operasyonu? .

Aynı şeyi çarpma için 64 yaptım ve aynı sonucu aldım. Bu yüzden muhtemelen derleyici işlemi tamamen görmezden geliyor, diğerleri dediği gibi testin değeri asla değişmedi.

Shiftwise Operatör Sonucu

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.