C'deki vardiya operatörlerini kullanarak çarpma ve bölme gerçekten daha hızlı mı?


288

Çarpma ve bölme, örneğin bit işleçleri kullanılarak gerçekleştirilebilir.

i*2 = i<<1
i*3 = (i<<1) + i;
i*10 = (i<<3) + (i<<1)

ve bunun gibi.

Diyelim ki (i<<3)+(i<<1)10 ile çarpmak i*10doğrudan kullanmaktan daha hızlı mıdır ? Bu şekilde çoğaltılamayan veya bölünemeyen herhangi bir girdi var mı?


8
Aslında, iki güçten başka bir sabitle ucuz bölme mümkündür, ancak sorunuzda "/ Bölme ... / bölünmüş" ile adalet yapmadığınız zor bir alt jet. Örneğin hackersdelight.org/divcMore.pdf adresine bakın (ya da mümkünse "Hacker'ın keyfi" kitabını alın).
Pascal Cuoq

46
Kolayca test edilebilen bir şey gibi geliyor.
juanchopanza

25
Her zamanki gibi - duruma göre değişir. Bir zamanlar bunu, bir çarpma işleminin bazilyon saatler aldığı bir Intel 8088 (IBM PC / XT) üzerinde montajcıda denedim. Vardiya ve eklemeler çok daha hızlı yürütüldü, bu yüzden iyi bir fikir gibi görünüyordu. Ancak, bus birimini çoğaltmak talimat sırasını doldurmakta özgürdü ve bir sonraki talimat hemen başlayabilirdi. Bir dizi vardiya ve ekleme işleminden sonra komut sırası boş olur ve CPU bir sonraki komutun bellekten alınmasını beklemek zorunda kalır (her seferinde bir bayt!). Ölçün, ölçün, ölçün!
Bo Persson

19
Ayrıca, sağa kaydırmanın yalnızca işaretsiz tamsayılar için iyi tanımlandığını unutmayın . İşaretli bir tamsayı varsa, 0 veya en yüksek bitin soldan doldurulup doldurulmadığı tanımlanmaz. (Ve bir başkasının (hatta kendiniz) bir yıl sonra kodu okuması için geçen zamanı unutmayın!)
Kerrek SB

29
Aslında, iyi bir optimize edici derleyici, daha hızlı olduklarında vardiyalarla çarpma ve bölme uygulayacaktır.
Peter G.

Yanıtlar:


487

Kısa cevap: Muhtemel değil.

Uzun cevap: Derleyicinizde, hedef işlemci mimarinizin yapabildiği kadar hızlı nasıl çoğaltılacağını bilen bir optimize edici vardır. En iyi seçeneğiniz derleyiciye niyetinizi açıkça anlatmak (i ​​<< 1 yerine i * 2) ve en hızlı montaj / makine kodu dizisinin ne olduğuna karar vermesine izin vermektir. İşlemcinin kendisinin çarpma komutunu mikrokodda bir vardiya ve ekleme dizisi olarak uygulaması bile mümkündür.

Alt satır - bunun için endişelenmek için çok fazla zaman harcamayın. Değişmek istiyorsan, değiş. Eğer çarpmak istiyorsan, çarp. Anlamsal olarak en açık olanı yapın - iş arkadaşlarınız size daha sonra teşekkür edecektir. Ya da, başka türlü yaparsanız daha sonra lanetleyin.


31
Evet, dediği gibi, hemen hemen her uygulama için olası kazanımlar, ortaya çıkan belirsizliğin üzerinde ağır basacaktır. Bu tür bir optimizasyondan önce endişelenmeyin. Matematiksel olarak net olanı oluşturun, darboğazları belirleyin ve oradan optimize edin ...
Dave

4
Kabul edilebilirlik, okunabilirlik ve sürdürülebilirlik için optimize etmek, muhtemelen profilin sıcak kod yolları olduğunu söyleyen şeyleri optimize etmek için daha fazla zaman harcayacaktır .
doug65536

5
Bu yorumlar, derleyiciye işini nasıl yapacağını söylemekten potansiyel performanstan vazgeçtiğinizi gibi hissettiriyor. Bu değil durum. Aslında x86'da shift sürümünden daha iyi kod alırsınız . Derleyiciye bakan biri çok çıktı (asm / optimizasyon cevaplarımın çoğuna bakın), şaşırmadım. Derleyiciyi bir şeyler yapmanın tek bir yolunda tutmaya yardımcı olabileceği zamanlar vardır , ancak bu onlardan biri değildir. gcc tamsayı matematikte iyidir, çünkü önemlidir. gcc -O3return i*10
Peter Cordes

Sadece bir arduino kroki indirildi millis() >> 2; Sadece bölmek istemek çok mu fazla olurdu?
Paul Wieland

1
Önce test i / 32v i >> 5ve i / 4v i >> 2optimizasyonu -O3 ile (herhangi bir donanım bölümü olan) korteks-A9 gcc ve montaj elde edilen tam olarak aynıydı. İlk önce bölümleri kullanmayı sevmedim ama niyetimi anlatıyor ve çıktı aynı.
robsn

91

Sadece somut bir ölçüm noktası: yıllar önce, karma algoritmamın iki versiyonunu karşılaştırdım:

unsigned
hash( char const* s )
{
    unsigned h = 0;
    while ( *s != '\0' ) {
        h = 127 * h + (unsigned char)*s;
        ++ s;
    }
    return h;
}

ve

unsigned
hash( char const* s )
{
    unsigned h = 0;
    while ( *s != '\0' ) {
        h = (h << 7) - h + (unsigned char)*s;
        ++ s;
    }
    return h;
}

Kıyasladığım her makinede, birincisi en azından ikincisi kadar hızlıydı. Biraz şaşırtıcı bir şekilde, bazen daha hızlıydı (örneğin, bir Sun Sparc'ta). Donanım hızlı çarpmayı desteklemediğinde (ve çoğu o zaman geri dönmedi), derleyici çarpmayı uygun vardiya ve add / sub kombinasyonlarına dönüştürür. Ve nihai hedefi bildiği için, bazen, vardiyaları ve add / subs'ları açıkça yazdığınızdan daha az talimatla yapabilirdi.

Bunun 15 yıl önce olduğu gibi bir şey olduğunu unutmayın. Umarım, derleyiciler sadece o zamandan beri daha iyi olmuştur, bu yüzden derhal doğru olanı yapan derleyiciye güvenebilirsiniz, muhtemelen yapabileceğinizden daha iyi. (Ayrıca, kodun bu kadar C'ish görünmesinin nedeni, 15 yıldan fazla bir süre önceydi çünkü std::stringbugün ve yineleyicileri kullanacağım .)


5
Yazar, modern optimizasyon derleyicilerinin, programcıların matematiksel formları üzerinde daha verimli düşünerek kullanabileceği ortak kalıpları gerçekten onlar için en verimli talimat dizisini oluşturmak için kullanabileceğini düşündüğü aşağıdaki blog yayınıyla ilgilenebilirsiniz. . shape-of-code.coding-guidelines.com/2009/06/30/…
Pascal Cuoq

@PascalCuoq Bu konuda gerçekten yeni bir şey yok. 20 yıl önce Sun CC için de aynı şeyi keşfettim.
James Kanze

67

Buradaki diğer tüm iyi cevaplara ek olarak, bölme veya çarpma anlamına gelirken kayma kullanmamanın başka bir nedenine dikkat edeyim. Bir zamanlar birinin çarpma ve toplama göreceli önceliğini unutarak bir hata getirdiğini görmedim. Bakım programcıları bir vardiya yoluyla "çarpma" mantıksal bir çarpma olduğunu ancak sözdizimsel olarak çarpma ile aynı önceliğe sahip olmadığını unutunca hatalar gördüm . x * 2 + zve x << 1 + zçok farklı!

Sayılar üzerinde çalışıyorsanız, gibi aritmetik işleçleri kullanın + - * / %. Bit dizileri üzerinde çalışıyorsanız, gibi bit döndürme işleçlerini kullanın & ^ | >>. Onları karıştırmayın; hem bit döndürme hem de aritmetik olan bir ifade olmasını bekleyen bir hatadır.


5
Basit parantez ile kaçınılmaz mı?
Joel B

21
@Joel: Tabii. Eğer onlara ihtiyacın olduğunu hatırlarsan. Demek istediğim, bunu unutmak kolay. Eğer "x * 1" sanki "x << 1" okuma zihinsel alışkanlığı olan insanlar, << değil çarpma ile aynı öncelik olduğunu düşünme zihinsel alışkanlığı almak.
Eric Lippert

1
Eh, "(hi << 8) + lo" ifadesini "hi * 256 + lo" dan daha fazla açıklayıcı buluyorum. Muhtemelen bir tat meselesidir, ancak bazen biraz burkulma yazmak daha açıktır. Çoğu durumda, tamamen sizin fikrinize katılıyorum.
Ivan Danilov

32
@Ivan: Ve "(merhaba << 8) | lo" daha da açıktır. Bir bit dizisinin düşük bitlerini ayarlamak tamsayıların eklenmesi değildir . Bitleri ayarlıyor , bu yüzden bitleri ayarlayan kodu yazın.
Eric Lippert

1
Vay. Daha önce böyle düşünmemiştim. Teşekkürler.
Ivan Danilov

50

Bu işlemciye ve derleyiciye bağlıdır. Bazı derleyiciler zaten kodu bu şekilde optimize ederken diğerleri derlemez. Bu nedenle, kodunuzun bu şekilde optimize edilmesi gerektiğinde kontrol etmeniz gerekir.

Umutsuzca optimize etmeniz gerekmedikçe, sadece bir montaj talimatı veya işlemci döngüsü kaydetmek için kaynak kodumu karıştırmayacağım.


3
Sadece kaba bir tahmin eklemek için: Tipik bir 16-Bit işlemcide (80C166) iki döngü eklemek 1-2 döngüde, 10 döngüde bir çarpma ve 20 döngüde bir bölme ile gelir. Ayrıca, i * 10'u birden fazla op'a optimize ederseniz bazı hareket işlemleri (her biri başka bir +1 döngüsü). En yaygın derleyiciler (Keil / Tasking) 2 gücü ile çarpma / bölme yapılmadığı sürece optimizasyon yapmazlar.
Jens

55
Ve genel olarak, derleyici kodu sizden daha iyi optimize eder.
user703016

"Miktarları" çarparken, çarpma operatörünün genellikle daha iyi olduğunu kabul ediyorum, ancak imzalı değerleri 2'lik güçlere böldüğünde, >>operatör daha hızlıdır /ve imzalı değerler negatif olabilirse, genellikle anlamsal olarak da üstündür. Biri x>>4üretecek değere ihtiyaç duyarsa , bu çok daha nettir x < 0 ? -((-1-x)/16)-1 : x/16;ve bir derleyicinin bu son ifadeyi güzel bir şeye nasıl optimize edebileceğini hayal edemiyorum.
supercat

38

10 ile çarpmak için say (i << 3) + (i << 1) kullanmak i * 10'u doğrudan kullanmaktan daha mı hızlı?

Makinenizde olabilir veya olmayabilir - eğer ilgilenirseniz, gerçek dünya kullanımınızı ölçün.

Bir vaka çalışması - 486'dan core i7'ye

Kıyaslama anlamlı olarak yapmak çok zordur, ancak birkaç gerçeğe bakabiliriz. Gönderen http://www.penguin.cz/~literakl/intel/s.html#SAL ve http://www.penguin.cz/~literakl/intel/i.html#IMUL biz x86 saat döngüsü hakkında bir fikir edinmek aritmetik kaydırma ve çarpma için gereklidir. Diyelim ki "486" (listelenen en yenisi), 32 bit kayıt ve hemen, IMUL 13-42 döngü ve IDIV 44'e sadık kalıyoruz. Her SAL 2 alır ve 1 ekler, bu yüzden birkaç tanesi birlikte yüzeysel olarak değişir. kazanan gibi.

Bugünlerde core i7 ile:

( http://software.intel.com/en-us/forums/showthread.php?t=61481 adresinden )

Gecikme, bir tamsayı ekleme için 1 döngü ve bir tamsayı çarpma için 3 döngüdür . Gecikmeleri ve düşünceleri http://www.intel.com/products/processor/manuals/ adresinde bulunan "Intel® 64 ve IA-32 Mimarileri Optimizasyon Referans Kılavuzu" nun Ek C'sinde bulabilirsiniz .

(bazı Intel bulanıklıklarından)

SSE'yi kullanarak Core i7, eşzamanlı ekleme ve çarpma talimatları yayınlayabilir, bu da saat döngüsü başına 8 kayan nokta işleminin (FLOP) en yüksek oranıyla sonuçlanır.

Bu size işlerin ne kadar ilerlediğine dair bir fikir verir. *90'larda bile ciddiye alınan optimizasyon trivia'sı - birazcık kaymaya karşı - artık kullanılmıyor. Bit kaydırma hala daha hızlıdır, ancak tüm vardiyalarınızı yaptığınızda ve sonuçları eklediğinizde iki mul / div'ın gücü olmayanlar için tekrar yavaşlar. Daha fazla talimat, daha fazla önbellek hatası, boru hattında daha fazla potansiyel sorun, geçici kayıtların daha fazla kullanılması, kayıt içeriğinin yığından daha fazla kaydedilmesi ve geri yüklenmesi anlamına gelebilir ... tüm etkileri kesin olarak ölçmek çok karmaşık hale gelir, ancak ağırlıklı olarak olumsuz.

kodunda uygulama ve işlevsellik karşılaştırması

Daha genel olarak, sorunuz C ve C ++ olarak etiketlenir. 3. nesil diller olarak, temeldeki CPU komut setinin ayrıntılarını gizlemek için özel olarak tasarlanmıştır. Dil Standartlarını karşılamak için , temel donanım olmasa bile çarpma ve kaydırma işlemlerini (ve diğerlerini) desteklemelidirler . Bu gibi durumlarda, diğer birçok talimatı kullanarak gerekli sonucu sentezlemelidirler. Benzer şekilde, CPU eksikse ve FPU yoksa kayan nokta işlemleri için yazılım desteği sağlamalıdırlar. Modern CPU'ların hepsi destekliyor* ve<<, bu yüzden saçma teorik ve tarihsel görünebilir, ancak önemli olan şey, uygulama seçme özgürlüğünün her iki yöne de gitmesidir: CPU, genel durumda kaynak kodunda istenen işlemi uygulayan bir talimata sahip olsa bile, derleyici tercih ettiği başka bir şey seçin çünkü derleyicinin karşılaştığı özel durum için daha iyidir .

Örnekler (varsayımsal bir montaj dili ile)

source           literal approach         optimised approach
#define N 0
int x;           .word x                xor registerA, registerA
x *= N;          move x -> registerA
                 move x -> registerB
                 A = B * immediate(0)
                 store registerA -> x
  ...............do something more with x...............

Exclusive veya ( xor) gibi yönergelerin kaynak koduyla hiçbir ilişkisi yoktur, ancak kendisiyle herhangi bir şeyin xoring edilmesi tüm bitleri temizler, bu nedenle 0 olarak bir şey ayarlamak için kullanılabilir. Bellek adreslerini ima eden kaynak kodu, herhangi bir kullanımı gerektirmeyebilir.

Bu tür bilgisayar korsanları bilgisayarlar olduğu sürece kullanılmaktadır. 3GL'lerin ilk günlerinde, geliştirici alımını güvence altına almak için derleyici çıktısı mevcut hardcore el optimizasyonlu montaj dili geliştiricisini tatmin etmek zorunda kaldı. üretilen kodun daha yavaş, daha ayrıntılı veya daha kötü olmadığı bir topluluk. Derleyiciler çok sayıda büyük optimizasyonu hızlı bir şekilde benimsedi - her bir montaj dili programcısının olabileceğinden daha iyi bir merkezi mağaza haline geldiler, ancak her zaman belirli bir durumda çok önemli olan belirli bir optimizasyonu kaçırmaları ihtimali var - insanlar bazen derleyin ve daha iyi bir şey için okuyorsun, derleyiciler de birileri bu deneyimleri geri besleyene kadar söylendiği gibi yaparlar.

Bu nedenle, bazı donanımlarda kaydırma ve ekleme hala daha hızlı olsa bile, derleyici yazarının hem güvenli hem de yararlı olduğunda tam olarak çalışmış olması muhtemeldir.

İdame

Donanımınız değişirse, yeniden derleyebilirsiniz ve hedef CPU'ya bakıp en iyi seçimi yaparsınız, oysa "optimizasyonlarınızı" tekrar gözden geçirmek veya hangi derleme ortamlarının çarpma ve hangilerinin değişmesi gerektiğini listelemek olası değildir. 10+ yıl önce yazılan, modern işlemcilerde çalıştıklarında içerdikleri kodu yavaşlatan, iki bit kaydırmalı "optimizasyonları" düşünün!

Neyse ki, GCC gibi iyi derleyiciler, herhangi bir optimizasyon etkinleştirildiğinde (yani ...main(...) { return (argc << 4) + (argc << 2) + argc; }-> imull $21, 8(%ebp), %eax) bir dizi bit kaydırma ve aritmetiği doğrudan bir çarpma ile değiştirebilir, böylece yeniden derleme kodu düzeltmeden bile yardımcı olabilir, ancak bu garanti edilmez.

Çarpma veya bölme uygulayan garip bitshifting kodu, kavramsal olarak elde etmeye çalıştığınız şeyden çok daha az ifade edicidir, bu nedenle diğer geliştiriciler bununla karıştırılacaktır ve şaşkın bir programcının, görünüşte akıl sağlığını geri kazanmak için gerekli olan şeyleri tanıtması veya önemli bir şeyi kaldırması daha olasıdır. Sadece bariz şeyler gerçekten somut olarak faydalı olduklarında yaparsanız ve sonra bunları iyi bir şekilde belgelerseniz (ancak yine de sezgisel olan diğer şeyleri belgelemiyorsanız), herkes daha mutlu olacaktır.

Kısmi çözümlere karşı genel çözümler

Aşağıdaki gibi bazı ekstra bilgi varsa senin o intirade gerçekten sadece değerleri depolamak x, yve z, o zaman bu değerler için çalışma ve derleyici en olmadığında daha çabuk size sonuç almak olduğunu bazı talimatlar çalışmak mümkün olabilir bu anlayış ve tüm intdeğerler için çalışan bir uygulamaya ihtiyaç duyar . Örneğin, sorunuzu düşünün:

Çarpma ve bölme, bit operatörleri kullanılarak gerçekleştirilebilir ...

Çarpmayı resmediyorsunuz, ama bölünmeye ne dersiniz?

int x;
x >> 1;   // divide by 2?

C ++ Standart 5.8'e göre:

-3- E1 >> E2'nin değeri E1 sağa kaydırılmış E2 bit konumudur. E1 imzasız bir türe sahipse veya E1 imzalı bir türe ve negatif olmayan bir değere sahipse, sonucun değeri E1 bölümünün E2 gücüne yükseltilen 2 miktarına bölünen parçasıdır. E1 imzalı bir türe ve negatif bir değere sahipse, elde edilen değer uygulama tanımlıdır.

Bu nedenle, bit kaydırma xnegatif olduğunda uygulama tanımlı bir sonuç verir : farklı makinelerde aynı şekilde çalışmayabilir. Ancak, /çok daha öngörülebilir bir şekilde çalışır. ( Farklı makineler farklı negatif sayılar temsiline sahip olabileceğinden ve dolayısıyla temsili oluşturan aynı sayıda bit olsa bile farklı aralıklara sahip olabileceğinden mükemmel bir şekilde tutarlı olmayabilir.)

"Umurumda değil ... bu intçalışanın yaşını saklıyor, asla olumsuz olamaz " diyebilirsiniz . Bu tür özel bir bilginiz varsa, evet - >>kodunuzda açıkça yapmadığınız sürece güvenli optimizasyonunuz derleyici tarafından aktarılabilir. Ancak, bu tür bir kavrayışa sahip olmayacağınız zamanlar riskli ve nadiren yararlıdır ve aynı kod üzerinde çalışan diğer programcılar, evinize verdiğiniz bazı olağandışı beklentiler üzerine bahse girdiğinizi bilemezler. "optimizasyonunuz" nedeniyle tamamen güvenli bir değişiklik geri tepebilir.

Bu şekilde çoğaltılamayan veya bölünemeyen herhangi bir girdi var mı?

Evet ... yukarıda belirtildiği gibi, negatif sayılar bit kaydırmaya "bölündüğünde" uygulama tanımlı davranışa sahiptir.


2
Çok güzel bir cevap. Core i7 ve 486 karşılaştırması aydınlatıcı!
Drew Hall

Tüm sıradan mimarilerde, bazen yararlı intVal>>1olanlardan farklı olan aynı semantiğe sahip olacaktır intVal/2. Eğer kişi, sıradan mimarilerin sağlayacağı değeri taşınabilir bir şekilde hesaplaması gerekirse intVal >> 1, ifadenin daha karmaşık ve okunması daha zor olması gerekir ve bunun üretilmesi için büyük ölçüde daha düşük bir kod üretmesi muhtemeldir intVal >> 1.
supercat

35

Bunu derleyerek makinemde denedim:

int a = ...;
int b = a * 10;

Sökürken çıktı üretir:

MOV EAX,DWORD PTR SS:[ESP+1C] ; Move a into EAX
LEA EAX,DWORD PTR DS:[EAX+EAX*4] ; Multiply by 5 without shift !
SHL EAX, 1 ; Multiply by 2 using shift

Bu sürüm, saf kaydırma ve ekleme ile elle optimize edilmiş kodunuzdan daha hızlıdır.

Derleyicinin ne ile geleceğini asla bilemezsiniz, bu yüzden derleyicinin optimize edemeyeceğini bildiğiniz çok kesin durumlar dışında, normal bir çarpma yazmak ve istediği şekilde optimize etmesine izin vermek daha iyidir .


1
Vektörle ilgili kısmı atlasaydınız bunun için büyük bir oy alırsınız. Derleyici çarpmayı düzeltebilirse, vektörün değişmediğini de görebilir.
Bo Persson

Bir derleyici, gerçekten tehlikeli varsayımlar yapmadan bir vektör boyutunun değişmeyeceğini nasıl bilebilir? Yoksa eşzamanlılığı hiç duymadınız mı ...
Charles Goodwin

1
Tamam, yani kilitleri olmayan küresel bir vektör üzerinde dönüyor musunuz? Ve adresi alınmamış bir yerel vektör üzerinde döngü yapıyorum ve sadece const üye işlevlerini çağırıyorum. En azından derleyicim vektör boyutunun değişmeyeceğini anlıyor. (ve yakında birisi muhtemelen sohbet için bizi işaretler :-).
Bo Persson

1
@BoPersson Son olarak, tüm bu süreden sonra, derleyicinin optimize edememesi ile ilgili ifademi kaldırdım vector<T>::size(). Derleyicim oldukça eskiydi! :)
user703016

21

Vites değiştirme genellikle bir talimat düzeyinde çarpmaktan çok daha hızlıdır, ancak erken optimizasyonlar yaparak zamanınızı boşa harcıyor olabilirsiniz. Derleyici bu optimizasyonları derleme sırasında iyi yapabilir. Bunu kendiniz yapmanız, okunabilirliği etkileyecek ve muhtemelen performans üzerinde bir etkisi olmayacaktır. Profilli ve bunu bir darboğaz olarak bulduysanız, muhtemelen sadece böyle şeyler yapmaya değer.

Aslında 'sihirli bölüm' olarak bilinen bölüm hilesi aslında büyük kazançlar sağlayabilir. Yine gerekli olup olmadığını görmek için önce profil oluşturmalısınız. Ancak bunu kullanırsanız, aynı bölüm semantiği için hangi talimatların gerekli olduğunu anlamanıza yardımcı olacak faydalı programlar vardır. İşte bir örnek : http://www.masm32.com/board/index.php?topic=12421.0

MASM32 OP'nin iş parçacığından kaldırdı bir örnek:

include ConstDiv.inc
...
mov eax,9999999
; divide eax by 100000
cdiv 100000
; edx = quotient

Şunu üretir:

mov eax,9999999
mov edx,0A7C5AC47h
add eax,1
.if !CARRY?
    mul edx
.endif
shr edx,16

7
@ Herhangi bir nedenle yorumunuz beni güldürdü ve kahvemi döktü. Teşekkürler.
asawyer

30
Matematiği beğenmeyle ilgili rastgele bir forum konusu yok. Matematiği seven herkes, gerçek bir "rastgele" forum dizisi oluşturmanın ne kadar zor olduğunu bilir.
Joel B

1
Muhtemelen sadece bunu bir darboğaz bulduysanız ve alternatifleri ve profili tekrar uygulayıp en az 10 kat performans avantajı elde ettiyseniz böyle şeyler yapmaya değer .
Yalan Ryan

12

Vardiya ve tamsayı çarpma talimatları çoğu modern CPU'da benzer performansa sahiptir - tamsayı çarpma talimatları 1980'lerde nispeten yavaştı, ancak genel olarak bu artık doğru değil. Tamsayı çarpma komutları daha yüksek gecikme süresine sahip olabilir , bu nedenle bir kaymanın tercih edilebileceği durumlar olabilir. Daha fazla yürütme birimini meşgul edebileceğiniz durumlar için Ditto (bu her iki yolu da kesebilir).

Tamsayı bölümü hala nispeten yavaştır, bu nedenle 2 gücüyle bölme yerine bir kaydırma kullanmak hala bir kazançtır ve çoğu derleyici bunu bir optimizasyon olarak uygulayacaktır. Ancak bu optimizasyonun geçerli olabilmesi için temettünün imzasız olması veya pozitif olarak bilinmesi gerektiğini unutmayın. Negatif temettü için, vardiya ve bölme eşdeğer değildir!

#include <stdio.h>

int main(void)
{
    int i;

    for (i = 5; i >= -5; --i)
    {
        printf("%d / 2 = %d, %d >> 1 = %d\n", i, i / 2, i, i >> 1);
    }
    return 0;
}

Çıktı:

5 / 2 = 2, 5 >> 1 = 2
4 / 2 = 2, 4 >> 1 = 2
3 / 2 = 1, 3 >> 1 = 1
2 / 2 = 1, 2 >> 1 = 1
1 / 2 = 0, 1 >> 1 = 0
0 / 2 = 0, 0 >> 1 = 0
-1 / 2 = 0, -1 >> 1 = -1
-2 / 2 = -1, -2 >> 1 = -1
-3 / 2 = -1, -3 >> 1 = -2
-4 / 2 = -2, -4 >> 1 = -2
-5 / 2 = -2, -5 >> 1 = -3

Derleyiciye yardım etmek istiyorsanız, temettüdeki değişkenin veya ifadenin açıkça imzasız olduğundan emin olun.


4
Tam sayı çarpanları, örneğin PlayStation 3'ün PPU'sunda mikro kodlanır ve tüm boru hattını durdurur. Hala bazı platformlarda tamsayı çarpımlarından kaçınılması önerilir :)
Maister

2
Birçok imzasız bölüm - derleyicinin nasıl bildiğini varsayarak - imzasız çarpanlar kullanılarak uygulanmaktadır. Bir veya iki çarpı @ birkaç saat döngüsünde, her biri ve her biri 40 döngüde aynı işi yapabilir.
Olof Forshell

1
@Olof: doğru, ancak yalnızca derleme zamanı sabiti ile bölünme için geçerlidir
Paul R

4

Tamamen hedef cihaza, dile, amaca vb. Bağlıdır.

Bir ekran kartı sürücüsünde piksel çatırtı mı? Büyük olasılıkla, evet!

Bölümünüz için .NET iş uygulaması? Kesinlikle içine bakmak için bir sebep yok.

Bir mobil cihaz için yüksek performanslı bir oyun için, ancak daha kolay optimizasyonlar yapıldıktan sonra incelenmeye değer olabilir.


2

Kesinlikle ihtiyacınız olmadıkça ve kod amacınız çarpma / bölme yerine kaydırma gerektirmezse yapmayın.

Tipik bir günde - birkaç makine döngüsünü potansiyel olarak kurtarabilirsiniz (veya derleyici neyi optimize edeceğini daha iyi bildiğinden), ancak maliyet buna değmez - kodun daha zor hale gelmesi, gerçek iş yerine küçük ayrıntılara zaman harcarsınız. iş arkadaşlarınız sizi lanetleyecek.

Kaydedilen her döngünün dakika çalışma süresi anlamına geldiği yüksek yüklü hesaplamalar için bunu yapmanız gerekebilir. Ancak, her seferinde bir yeri optimize etmeli ve gerçekten daha hızlı hale getirip getirmediğinizi veya derleyici mantığını kırdığınızı görmek için her seferinde performans testleri yapmalısınız.


1

Bildiğim kadarıyla bazı makinelerde çarpmanın 16 ila 32 makine döngüsüne kadar ihtiyacı olabilir. Yani Evet , makine tipine bağlı olarak bitshift operatörler daha hızlı çarpma / bölme daha vardır.

Bununla birlikte, bazı makinelerin çarpma / bölme için özel talimatlar içeren matematik işlemcileri vardır.


7
Bu makineler için derleyiciler yazan kişiler muhtemelen Hacker Delight'ı okudular ve buna göre optimize ettiler.
Bo Persson

1

Drew Hall'un işaretli cevabına katılıyorum. Cevap bazı ek notlar kullanabilir.

Yazılım geliştiricilerinin büyük çoğunluğu için işlemci ve derleyici artık soruyla ilgili değildir. Çoğumuz 8088 ve MS-DOS'un çok ötesindeyiz. Belki de sadece gömülü işlemciler için hala gelişmekte olanlar için geçerlidir ...

Yazılım şirketimde matematik (add / sub / mul / div) tüm matematik için kullanılmalıdır. Veri türleri arasında dönüştürme yaparken Shift kullanılmalıdır. bayt'a n / 256 değil , n >> 8 olarak bayt .


Ben de sana katılıyorum. Aynı kılavuzu bilinçaltı olarak izliyorum, ancak bunu yapmak için resmi bir gereksinimim olmadı.
Drew Hall

0

İşaretli tamsayılar ve sağa kayma ile bölünme durumunda, fark yaratabilir. Negatif sayılar için, kaydırma yuvarları negatif sonsuza yuvarlanırken, bölme sıfıra yuvarlar. Tabii ki derleyici bölümü daha ucuz bir şeye değiştirecek, ancak genellikle bölümle aynı yuvarlama davranışına sahip bir şeye değiştirecektir, çünkü ya değişkenin negatif olmayacağını kanıtlayamaz ya da basitçe bakım. Dolayısıyla, bir sayının negatif olmayacağını kanıtlayabilirseniz veya hangi yöne döneceğini umursamıyorsanız, bu optimizasyonu fark yaratma olasılığı daha yüksek bir şekilde yapabilirsiniz.


veya numarayıunsigned
Lie Ryan

4
Vites değiştirme davranışının standartlaştırıldığından emin misiniz? Negatif içe doğru sağ kaymanın uygulama tanımlı olduğu izlenimindeydim.
Kerrek SB

1
Belki de sağa kayma negatif sayıları için herhangi bir özel davranışa dayanan kodun bu gereksinimi belgelemesi gerektiğini belirtmekle birlikte, doğal olarak doğru değeri verdiği ve bölme operatörünün atık kodunu üreteceği durumlarda sağa kaydırma avantajı çok büyüktür. kullanıcı kodunun daha sonra vardiyada ne vereceğini vermek için ek zaman ayarlaması yapmak zorunda kalacağı istenmeyen bir değeri hesaplama zamanı. Aslında, eğer druthers'ım olsaydı, derleyiciler imzalı bölümü gerçekleştirme girişimlerinde squawk yapma seçeneğine sahip olacaklardı, çünkü ...
supercat

1
... işlenenlerin pozitif olduğunu bilen kod, bölünmeden önce imzasız hale getirilirse (muhtemelen daha sonra imzalanmışa geri dönüyorsa) optimizasyonu iyileştirebilir ve işlenenlerin negatif olabileceğini bilen kod genellikle bu durumla açıkça ilgilenmelidir (bu durumda) kişi de olumlu olduklarını varsayabilir).
supercat

0

Aynı rastgele sayılara karşı 100 milyon kez aynı çarpımı yapan Python testi.

>>> from timeit import timeit
>>> setup_str = 'import scipy; from scipy import random; scipy.random.seed(0)'
>>> N = 10*1000*1000
>>> timeit('x=random.randint(65536);', setup=setup_str, number=N)
1.894096851348877 # Time from generating the random #s and no opperati

>>> timeit('x=random.randint(65536); x*2', setup=setup_str, number=N)
2.2799630165100098
>>> timeit('x=random.randint(65536); x << 1', setup=setup_str, number=N)
2.2616429328918457

>>> timeit('x=random.randint(65536); x*10', setup=setup_str, number=N)
2.2799630165100098
>>> timeit('x=random.randint(65536); (x << 3) + (x<<1)', setup=setup_str, number=N)
2.9485139846801758

>>> timeit('x=random.randint(65536); x // 2', setup=setup_str, number=N)
2.490908145904541
>>> timeit('x=random.randint(65536); x / 2', setup=setup_str, number=N)
2.4757170677185059
>>> timeit('x=random.randint(65536); x >> 1', setup=setup_str, number=N)
2.2316000461578369

Bu nedenle, python'daki iki güçle çarpma / bölme yerine bir kaydırma yaparken, hafif bir iyileşme vardır (bölme için ~% 10; çarpma için ~% 1). Eğer ikisinin gücü değilse, muhtemelen önemli bir yavaşlama olur.

Yine bu #'ler işlemcinize, derleyicinize (veya yorumlayıcıya - basitlik için python'da) bağlı olarak değişecektir.

Diğer herkes gibi, erken optimizasyon yapmayın. Çok okunabilir kod yazın, yeterince hızlı değilse profil oluşturun ve yavaş parçaları optimize etmeye çalışın. Unutmayın, derleyiciniz optimizasyonda sizden çok daha iyidir.


0

Derleyicinin yapamayacağı optimizasyonlar vardır, çünkü yalnızca daha düşük bir girdi kümesi için çalışırlar.

Aşağıda 64 bit "Karşılıklı çarpma" yaparak daha hızlı bir bölüm yapabilen c ++ örnek kodu vardır. Hem pay hem de payda belirli eşiğin altında olmalıdır. Aslında normal bölümden daha hızlı olması için 64 bitlik talimatların derlenmesi gerektiğini unutmayın.

#include <stdio.h>
#include <chrono>

static const unsigned s_bc = 32;
static const unsigned long long s_p = 1ULL << s_bc;
static const unsigned long long s_hp = s_p / 2;

static unsigned long long s_f;
static unsigned long long s_fr;

static void fastDivInitialize(const unsigned d)
{
    s_f = s_p / d;
    s_fr = s_f * (s_p - (s_f * d));
}

static unsigned fastDiv(const unsigned n)
{
    return (s_f * n + ((s_fr * n + s_hp) >> s_bc)) >> s_bc;
}

static bool fastDivCheck(const unsigned n, const unsigned d)
{
    // 32 to 64 cycles latency on modern cpus
    const unsigned expected = n / d;

    // At least 10 cycles latency on modern cpus
    const unsigned result = fastDiv(n);

    if (result != expected)
    {
        printf("Failed for: %u/%u != %u\n", n, d, expected);
        return false;
    }

    return true;
}

int main()
{
    unsigned result = 0;

    // Make sure to verify it works for your expected set of inputs
    const unsigned MAX_N = 65535;
    const unsigned MAX_D = 40000;

    const double ONE_SECOND_COUNT = 1000000000.0;

    auto t0 = std::chrono::steady_clock::now();
    unsigned count = 0;
    printf("Verifying...\n");
    for (unsigned d = 1; d <= MAX_D; ++d)
    {
        fastDivInitialize(d);
        for (unsigned n = 0; n <= MAX_N; ++n)
        {
            count += !fastDivCheck(n, d);
        }
    }
    auto t1 = std::chrono::steady_clock::now();
    printf("Errors: %u / %u (%.4fs)\n", count, MAX_D * (MAX_N + 1), (t1 - t0).count() / ONE_SECOND_COUNT);

    t0 = t1;
    for (unsigned d = 1; d <= MAX_D; ++d)
    {
        fastDivInitialize(d);
        for (unsigned n = 0; n <= MAX_N; ++n)
        {
            result += fastDiv(n);
        }
    }
    t1 = std::chrono::steady_clock::now();
    printf("Fast division time: %.4fs\n", (t1 - t0).count() / ONE_SECOND_COUNT);

    t0 = t1;
    count = 0;
    for (unsigned d = 1; d <= MAX_D; ++d)
    {
        for (unsigned n = 0; n <= MAX_N; ++n)
        {
            result += n / d;
        }
    }
    t1 = std::chrono::steady_clock::now();
    printf("Normal division time: %.4fs\n", (t1 - t0).count() / ONE_SECOND_COUNT);

    getchar();
    return result;
}

0

Ben bir durumda iki güç ile çarpmak veya bölmek istediğiniz düşünüyorum, derleyici onları bir MUL / DIV dönüştürür bile, çünkü bazı işlemciler mikro kod (gerçekten, bir Bu durumda, özellikle vardiya 1'den fazlaysa bir iyileştirme elde edersiniz. Ya da daha açık bir şekilde, CPU'nun bitshift operatörleri yoksa, yine de bir MUL / DIV olacaktır, ancak CPU bitshift operatörleri, bir mikrokod dalından kaçınırsınız ve bu birkaç talimat daha azdır.

Şu anda yoğun bir ikili ağaç üzerinde çalıştığı için iki katlama / yarıya indirme işlemi gerektiren bir kod yazıyorum ve bir ekten daha iyi olabileceğinden şüphelendiğim bir işlem daha var - bir sol (iki katın gücü) ) bir ekleme ile kaydırın. Bu, sola kaydırma ve xor ile değiştirilebilir, eğer kaydırma eklemek istediğiniz bit sayısından daha genişse, kolay örnek (i << 1) ^ 1'dir, bu da iki katına çıkarılan bir değere bir değer katar. Bu, elbette sağa kaydırma (iki bölmenin gücü) için geçerli değildir, çünkü sadece bir sol (küçük endian) kaydırma boşluğu sıfırlarla doldurur.

Kodumda, bu iki ile çarpma / bölme ve iki işlemin gücü çok yoğun bir şekilde kullanılıyor ve formüller zaten çok kısa olduğu için, ortadan kaldırılabilen her talimat önemli bir kazanç olabilir. İşlemci bu bit kaydırma operatörlerini desteklemiyorsa, kazanç olmaz, ancak kayıp olmaz.

Ayrıca, yazdığım algoritmalarda, ortaya çıkan hareketleri görsel olarak temsil ederler, bu yüzden aslında daha açıktırlar. İkili ağacın sol tarafı daha büyük, sağ tarafı daha küçüktür. Bunun yanı sıra, benim kodumda, tek ve çift sayılar özel bir öneme sahiptir ve ağaçtaki tüm sol çocuklar tuhaftır ve sağ çocuklar ve kök de çifttir. Henüz karşılaşmadığım bazı durumlarda, ama, aslında, bunu düşünmedim bile, x & 1, x% 2'ye kıyasla daha uygun bir işlem olabilir. Çift sayıdaki x & 1 sıfır üretecek, ancak tek bir sayı için 1 üretecektir.

Sadece tek / çift tanımlamadan biraz daha ileri gidersem, x & 3 için sıfır alırsam, 4'ün sayımızın bir faktörü olduğunu ve 8 için x% 7 için aynı olduğunu biliyorum. Bu durumların muhtemelen sınırlı bir faydası olduğunu biliyorum, ancak bir modül işleminden kaçınabileceğinizi ve bunun yerine bitsel bir mantık işlemi kullanabileceğinizi bilmek güzel, çünkü bitsel işlemler neredeyse her zaman en hızlı ve en az derleyiciye belirsiz.

Çok yoğun ikili ağaçlar alanını icat ediyorum, bu yüzden insanların bu yorumun değerini kavramayacağını umuyorum, çünkü çok nadiren insanlar sadece iki güç üzerinde çarpanlara ayırma yapmak ya da sadece ikinin güçlerini çarpmak / bölmek istiyorlar.



0

Bir gcc derleyicisindeki x + x, x * 2 ve x << 1 sözdizimi için çıktıyı karşılaştırırsanız, aynı sonucu x86 derlemesinde alırsınız: https://godbolt.org/z/JLpp0j

        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-4], edi
        mov     eax, DWORD PTR [rbp-4]
        add     eax, eax
        pop     rbp
        ret

Böylece gcc'yi yazdığınız şeyden bağımsız olarak kendi en iyi çözümünü belirlemek için akıllıca düşünebilirsiniz .


0

Ben de Evi yenebilir miyim görmek istedim. herhangi bir sayı çarpımı ile herhangi bir sayı için daha genel bir bitseldir. yaptığım makrolar normal çarpma işleminden yaklaşık% 25 daha fazla ila iki kat daha yavaştır. başkalarının söylediği gibi 2'nin katlarına yakınsa veya 2'nin birkaç katından oluşursa kazanabilirsiniz. (X << 4) + (X << 2) + (X << 1) + X'den oluşan X * 23 gibi (X << 6) + X'den oluşan X * 65 daha yavaş olacaktır.

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

#define MULTIPLYINTBYMINUS(X,Y) (-((X >> 30) & 1)&(Y<<30))+(-((X >> 29) & 1)&(Y<<29))+(-((X >> 28) & 1)&(Y<<28))+(-((X >> 27) & 1)&(Y<<27))+(-((X >> 26) & 1)&(Y<<26))+(-((X >> 25) & 1)&(Y<<25))+(-((X >> 24) & 1)&(Y<<24))+(-((X >> 23) & 1)&(Y<<23))+(-((X >> 22) & 1)&(Y<<22))+(-((X >> 21) & 1)&(Y<<21))+(-((X >> 20) & 1)&(Y<<20))+(-((X >> 19) & 1)&(Y<<19))+(-((X >> 18) & 1)&(Y<<18))+(-((X >> 17) & 1)&(Y<<17))+(-((X >> 16) & 1)&(Y<<16))+(-((X >> 15) & 1)&(Y<<15))+(-((X >> 14) & 1)&(Y<<14))+(-((X >> 13) & 1)&(Y<<13))+(-((X >> 12) & 1)&(Y<<12))+(-((X >> 11) & 1)&(Y<<11))+(-((X >> 10) & 1)&(Y<<10))+(-((X >> 9) & 1)&(Y<<9))+(-((X >> 8) & 1)&(Y<<8))+(-((X >> 7) & 1)&(Y<<7))+(-((X >> 6) & 1)&(Y<<6))+(-((X >> 5) & 1)&(Y<<5))+(-((X >> 4) & 1)&(Y<<4))+(-((X >> 3) & 1)&(Y<<3))+(-((X >> 2) & 1)&(Y<<2))+(-((X >> 1) & 1)&(Y<<1))+(-((X >> 0) & 1)&(Y<<0))
#define MULTIPLYINTBYSHIFT(X,Y) (((((X >> 30) & 1)<<31)>>31)&(Y<<30))+(((((X >> 29) & 1)<<31)>>31)&(Y<<29))+(((((X >> 28) & 1)<<31)>>31)&(Y<<28))+(((((X >> 27) & 1)<<31)>>31)&(Y<<27))+(((((X >> 26) & 1)<<31)>>31)&(Y<<26))+(((((X >> 25) & 1)<<31)>>31)&(Y<<25))+(((((X >> 24) & 1)<<31)>>31)&(Y<<24))+(((((X >> 23) & 1)<<31)>>31)&(Y<<23))+(((((X >> 22) & 1)<<31)>>31)&(Y<<22))+(((((X >> 21) & 1)<<31)>>31)&(Y<<21))+(((((X >> 20) & 1)<<31)>>31)&(Y<<20))+(((((X >> 19) & 1)<<31)>>31)&(Y<<19))+(((((X >> 18) & 1)<<31)>>31)&(Y<<18))+(((((X >> 17) & 1)<<31)>>31)&(Y<<17))+(((((X >> 16) & 1)<<31)>>31)&(Y<<16))+(((((X >> 15) & 1)<<31)>>31)&(Y<<15))+(((((X >> 14) & 1)<<31)>>31)&(Y<<14))+(((((X >> 13) & 1)<<31)>>31)&(Y<<13))+(((((X >> 12) & 1)<<31)>>31)&(Y<<12))+(((((X >> 11) & 1)<<31)>>31)&(Y<<11))+(((((X >> 10) & 1)<<31)>>31)&(Y<<10))+(((((X >> 9) & 1)<<31)>>31)&(Y<<9))+(((((X >> 8) & 1)<<31)>>31)&(Y<<8))+(((((X >> 7) & 1)<<31)>>31)&(Y<<7))+(((((X >> 6) & 1)<<31)>>31)&(Y<<6))+(((((X >> 5) & 1)<<31)>>31)&(Y<<5))+(((((X >> 4) & 1)<<31)>>31)&(Y<<4))+(((((X >> 3) & 1)<<31)>>31)&(Y<<3))+(((((X >> 2) & 1)<<31)>>31)&(Y<<2))+(((((X >> 1) & 1)<<31)>>31)&(Y<<1))+(((((X >> 0) & 1)<<31)>>31)&(Y<<0))
int main()
{
    int randomnumber=23;
    int randomnumber2=23;
    int checknum=23;
    clock_t start, diff;
    srand(time(0));
    start = clock();
    for(int i=0;i<1000000;i++)
    {
        randomnumber = rand() % 10000;
        randomnumber2 = rand() % 10000;
        checknum=MULTIPLYINTBYMINUS(randomnumber,randomnumber2);
        if (checknum!=randomnumber*randomnumber2)
        {
            printf("s %i and %i and %i",checknum,randomnumber,randomnumber2);
        }
    }
    diff = clock() - start;
    int msec = diff * 1000 / CLOCKS_PER_SEC;
    printf("MULTIPLYINTBYMINUS Time %d milliseconds", msec);
    start = clock();
    for(int i=0;i<1000000;i++)
    {
        randomnumber = rand() % 10000;
        randomnumber2 = rand() % 10000;
        checknum=MULTIPLYINTBYSHIFT(randomnumber,randomnumber2);
        if (checknum!=randomnumber*randomnumber2)
        {
            printf("s %i and %i and %i",checknum,randomnumber,randomnumber2);
        }
    }
    diff = clock() - start;
    msec = diff * 1000 / CLOCKS_PER_SEC;
    printf("MULTIPLYINTBYSHIFT Time %d milliseconds", msec);
    start = clock();
    for(int i=0;i<1000000;i++)
    {
        randomnumber = rand() % 10000;
        randomnumber2 = rand() % 10000;
        checknum= randomnumber*randomnumber2;
        if (checknum!=randomnumber*randomnumber2)
        {
            printf("s %i and %i and %i",checknum,randomnumber,randomnumber2);
        }
    }
    diff = clock() - start;
    msec = diff * 1000 / CLOCKS_PER_SEC;
    printf("normal * Time %d milliseconds", msec);
    return 0;
}
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.