<< >> çarpma ve bölme hızları


9

Sayıları <<python'da çoğaltmak ve >>bölmek için kullanabilirsiniz . Zaman ayırdığımda ikili kaydırma yolunu kullanarak normal yolu bölmekten veya çarpmaktan 10 kat daha hızlıdır.

Neden kullanıyor <<ve >>çok daha hızlı *ve /?

Sahne arkası süreçleri neler yapıyor *ve /bu kadar yavaş?


2
Bit kaydırma yalnızca Python'da değil, tüm dillerde daha hızlıdır. Birçok işlemci, bir veya iki saat döngüsünde bunu başaracak yerel bir bit kaydırma komutuna sahiptir.
Robert Harvey

4
Bununla birlikte, normal bölme ve çarpma operatörlerini kullanmak yerine bit değiştirmenin genellikle kötü bir uygulama olduğu ve okunabilirliği engelleyebileceği unutulmamalıdır.
Azar

6
@crizly Çünkü en iyi ihtimalle bir mikro optimizasyon ve derleyicinin yine de bayt kodundaki bir kaymaya (mümkünse) geçmesi için iyi bir şans var. Kodun performans açısından son derece kritik olduğu durumlar gibi istisnalar vardır, ancak çoğu zaman yaptığınız şey kodunuzu gizlemektir.
Azar

7
@Crizly: İyi bir optimize ediciye sahip herhangi bir derleyici, bit kaydırmalarıyla yapılabilecek çarpmaları ve bölümleri tanıyacak ve bunları kullanan kod üretecektir. Derleyiciyi alt etmeye çalışırken kodunuzu çirkinleştirmeyin.
Blrfl

2
In StackOverflow'daki bu soruya bir microbenchmark biraz bulundu daha iyi yeterince küçük sayılar için, eşdeğer bir sol vardiyası için 2'den tarafından çoğalması için Python 3'te performansı. Bence küçük çarpmaların (şu anda) bit kaydırmalarından farklı olarak optimize edilmesine kadar izini sürdüm. Sadece teoriye göre daha hızlı çalışacak olanı kabul edemeyeceğinizi gösteriyor.
Dan Getz

Yanıtlar:


15

Biraz kaydırma ve bölme yapan iki küçük C programına bakalım.

#include <stdlib.h>

int main(int argc, char* argv[]) {
        int i = atoi(argv[0]);
        int b = i << 2;
}
#include <stdlib.h>

int main(int argc, char* argv[]) {
        int i = atoi(argv[0]);
        int d = i / 4;
}

Bunların her biri gcc -S, gerçek montajın ne olacağını görmek için derlenir .

Bit kaydırma sürümünde, çağrıdan atoigeri dönmeye:

    callq   _atoi
    movl    $0, %ecx
    movl    %eax, -20(%rbp)
    movl    -20(%rbp), %eax
    shll    $2, %eax
    movl    %eax, -24(%rbp)
    movl    %ecx, %eax
    addq    $32, %rsp
    popq    %rbp
    ret

Bölme sürümü sırasında:

    callq   _atoi
    movl    $0, %ecx
    movl    $4, %edx
    movl    %eax, -20(%rbp)
    movl    -20(%rbp), %eax
    movl    %edx, -28(%rbp)         ## 4-byte Spill
    cltd
    movl    -28(%rbp), %r8d         ## 4-byte Reload
    idivl   %r8d
    movl    %eax, -24(%rbp)
    movl    %ecx, %eax
    addq    $32, %rsp
    popq    %rbp
    ret

Sadece buna bakarak bölme sürümünde bit kaydırmaya kıyasla birkaç talimat daha var.

Anahtar ne yapıyorlar?

Bit kaydırma sürümünde, anahtar talimat shll $2, %eaxmantıksal bir sola kaydırmadır - bölünme vardır ve diğer her şey değerleri sadece hareket ettirir.

Bölme sürümünde, görebilirsiniz idivl %r8d- ancak bunun üzerinde cltd, dökülme ve yeniden yükleme etrafında bir (uzuntan ikiye dönüştür) ve bazı ek mantık var. Bu ek çalışma, sadece biraz matematik yaparak oluşabilecek çeşitli hatalardan kaçınmak için bitlerden ziyade bir matematikle uğraştığımızı bilmek gerekir.

Hızlı bir çarpma yapalım:

#include <stdlib.h>

int main(int argc, char* argv[]) {
    int i = atoi(argv[0]);
    int b = i >> 2;
}
#include <stdlib.h>

int main(int argc, char* argv[]) {
    int i = atoi(argv[0]);
    int d = i * 4;
}

Tüm bunlardan geçmek yerine, farklı bir satır var:

$ diff mult.s bit.s
24c24
> shll $ 2,% eax
---
<sarl $ 2,% eax

Burada derleyici matematiğin bir kayma ile yapılabileceğini belirleyebildi, ancak mantıksal bir kayma yerine aritmetik bir kayma yapıyor. Bunları çalıştırırsak bunlar arasındaki fark açıktır - sarlişareti korur. Böylece -2 * 4 = -8ederken shlldeğildir.

Buna hızlı bir perl betiği ile bakalım:

#!/usr/bin/perl

$foo = 4;
print $foo << 2, "\n";
print $foo * 4, "\n";

$foo = -4;
print $foo << 2, "\n";
print $foo * 4, "\n";

Çıktı:

16
16
18446744073709551600
-16

Um ... -4 << 2olan 18446744073709551600çarpma ve bölme ile uğraşırken muhtemelen bekliyoruz tam olarak ne olmadığı. Haklı, ama tamsayı çarpımı değil.

Ve böylece erken optimizasyona karşı dikkatli olun. Derleyicinin sizin için optimize etmesine izin verin - gerçekten ne yapmaya çalıştığınızı bilir ve daha az hata ile muhtemelen daha iyi bir iş çıkarır.


12
Eşleştirmek için daha net olabilir << 2ile * 4ve >> 2ile / 4Her örnekte içindeki vardiya Tarif aynı tutmak için.
Greg Hewgill

6

Mevcut cevaplar, şeylerin donanım tarafına gerçekten değinmedi, bu yüzden bu açıdan biraz. Geleneksel bilgelik, çarpma ve bölmenin değişmekten çok daha yavaş olmasıdır, ancak bugünkü gerçek hikaye daha nüanslıdır.

Örneğin, çarpmanın donanımda uygulamak için daha karmaşık bir işlem olduğu kesinlikle doğrudur , ancak her zaman daha yavaş sonuçlanmayabilir . Sonuç olarak, adduygulanması xor(veya genel olarak herhangi bir bitsel işlemden) önemli ölçüde daha karmaşıktır , ancak add(ve sub) genellikle işlemlerine adanmış bitis operatörleri kadar hızlı olan yeterli transistör elde eder. Böylece donanım uygulama karmaşıklığına sadece hız için bir rehber olarak bakamazsınız.

Öyleyse, çarpma ve kaydırma gibi "tam" operatörlere karşı kaymaya ayrıntılı olarak bakalım.

değişken

Neredeyse tüm donanımlarda, sabit bir miktarda (yani derleyicinin derleme zamanında belirleyebileceği bir miktarda) kaydırma hızlıdır . Özellikle, genellikle tek bir döngü gecikmesi ve döngü başına 1 veya daha iyi bir verim ile gerçekleşir. Bazı donanımlarda (örneğin, bazı Intel ve ARM yongaları), bir sabit tarafından belirli kaymalar "ücretsiz" bile olabilir, çünkü bunlar başka bir talimata da yerleştirilebilir ( leaIntel'de, ARM'deki ilk kaynağın özel kayma yetenekleri).

Değişken miktarda kaydırma, daha çok gri bir alandır. Eski donanımlarda bu bazen çok yavaştı ve hız nesilden nesile değişti. Örneğin, Intel'in P4'ün ilk sürümünde, değişken bir miktarda vites değiştirmek kötü bir şekilde yavaştı - vardiya miktarıyla orantılı zaman gerektiriyordu! Bu platformda, vardiyaların yerini almak için çarpımları kullanmak kârlı olabilir (yani, dünya tersine döndü). Önceki Intel yongalarında ve sonraki nesillerde, değişken bir miktarda kayma o kadar acı verici değildi.

Mevcut Intel yongalarında, değişken bir miktarda değiştirmek özellikle hızlı değildir, ancak korkunç değildir. Değişken vardiyalar söz konusu olduğunda x86 mimarisi hamstung, çünkü işlemi alışılmadık bir şekilde tanımladılar: vardiya 0 miktarları koşul bayraklarını değiştirmez, ancak diğer tüm vardiyalar yapar. Bu, bayrak kaydının etkin bir şekilde yeniden adlandırılmasını engeller, çünkü vardiya sonraki komutların vardiya tarafından yazılan koşul kodlarını mı yoksa bir önceki talimatı mı okuması gerektiğini belirleyene kadar belirlenemez. Ayrıca, vardiyalar sadece bayrak yazmacının bir kısmına yazarak kısmi bayrak duraklamasına neden olabilir.

Sonuç olarak, son Intel mimarilerinde, değişken bir miktarda kaydırmanın üç "mikro işlem" alması, diğer basit işlemlerin (ekleme, bitsel ops, hatta çarpma) yalnızca 1 almasıdır. .

Çarpma işlemi

Modern masaüstü ve dizüstü bilgisayar donanımındaki eğilim, çarpmayı hızlı bir işlem haline getirmektir. Son Intel ve AMD yongalarında, aslında, her döngüde bir çarpma verilebilir (buna karşılıklı verim diyoruz ). Gecikme , ancak, bir çarpma 3 döngüleri. Yani aracı almak olduğunu sonucunu başlattıktan sonra herhangi bir çarpma 3 döngü, ancak yeni bir çarpma her döngüyü başlatmak edebiliyoruz. Hangi değerin (1 döngü veya 3 döngü) daha önemli olduğu algoritmanızın yapısına bağlıdır. Çarpma kritik bir bağımlılık zincirinin parçasıysa, gecikme önemlidir. Değilse, karşılıklı verim veya diğer faktörler daha önemli olabilir.

Onlar anahtar paket servisi olan şey, modern dizüstü bilgisayar yongalarında (veya daha iyisi), çarpmanın hızlı bir işlem olması ve bir derleyicinin gücü azaltılmış vardiyalar için "yuvarlama" hakkını vereceği 3 veya 4 komut dizisinden daha hızlı olmasıdır. Değişken kaymalar için, Intel'de, yukarıda belirtilen sorunlar nedeniyle çarpma da genellikle tercih edilir.

Daha küçük form faktörlü platformlarda, tam ve hızlı bir 32 bit veya özellikle 64 bit çarpan oluşturmak çok fazla transistör ve güç gerektirdiğinden, çarpma hala daha yavaş olabilir. Birisi son mobil çiplerde çarpma performansının ayrıntılarını doldurabilirse çok takdir edilecektir.

bölmek

Bölme, hem çarpma işleminden daha donanımsal olarak daha karmaşık bir işlemdir ve aynı zamanda gerçek kodda çok daha az yaygındır - yani ona daha az kaynak tahsis edilir. Modern yongalardaki eğilim hala daha hızlı bölücülere doğru, ancak modern üst düzey yongalar bile bir bölünme yapmak için 10-40 döngü alıyor ve sadece kısmen boru hattında. Genel olarak, 64 bitlik bölünmeler 32 bitlik bölünmelerden bile daha yavaştır. Diğer birçok işlemden farklı olarak, bölme bağımsız değişkenlere bağlı olarak değişken sayıda döngü alabilir.

Ayırmaktan kaçının ve vardiyalarla değiştirin (veya derleyicinin yapmasına izin verin, ancak montajı kontrol etmeniz gerekebilir)!


2

BINARY_LSHIFT ve BINARY_RSHIFT algoritmik olarak BINARY_MULTIPLY ve BINARY_FLOOR_DIVIDE işlemlerinden daha basit süreçlerdir ve daha az saat döngüsü alabilirler. Yani herhangi bir ikili numaranız varsa ve N'ye bitshift yapmanız gerekiyorsa, yapmanız gereken tek şey rakamları bu boşluklara kaydırmak ve sıfırlarla değiştirmek. İkili çarpma genellikle daha karmaşıktır , ancak Dadda çarpanı gibi teknikler onu oldukça hızlı hale getirir.

Verilen bir optimizasyon derleyicisinin, iki güçle çarptığında / bölündüğünde ve uygun sola / sağa kaydırmayla değiştirdiğinizde durumları tanıması mümkündür. Demonte bayt kodu python bakarak görünüşte bunu yapmaz:

>>> dis.dis(lambda x: x*4)
  1           0 LOAD_FAST                0 (x)
              3 LOAD_CONST               1 (4)
              6 BINARY_MULTIPLY     
              7 RETURN_VALUE        

>>> dis.dis(lambda x: x<<2)
  1           0 LOAD_FAST                0 (x)
              3 LOAD_CONST               1 (2)
              6 BINARY_LSHIFT       
              7 RETURN_VALUE        


>>> dis.dis(lambda x: x//2)
  1           0 LOAD_FAST                0 (x)
              3 LOAD_CONST               1 (2)
              6 BINARY_FLOOR_DIVIDE 
              7 RETURN_VALUE        

>>> dis.dis(lambda x: x>>1)
  1           0 LOAD_FAST                0 (x)
              3 LOAD_CONST               1 (1)
              6 BINARY_RSHIFT       
              7 RETURN_VALUE        

Ancak, işlemcimde çarpma ve sol / sağ kaydırmanın benzer zamanlamaya sahip olduğunu ve kat bölümünün (iki güçle) yaklaşık% 25 daha yavaş olduğunu görüyorum:

>>> import timeit

>>> timeit.repeat("z=a + 4", setup="a = 37")
[0.03717184066772461, 0.03291916847229004, 0.03287005424499512]

>>> timeit.repeat("z=a - 4", setup="a = 37")
[0.03534698486328125, 0.03207516670227051, 0.03196907043457031]

>>> timeit.repeat("z=a * 4", setup="a = 37")
[0.04594111442565918, 0.0408930778503418, 0.045324087142944336]

>>> timeit.repeat("z=a // 4", setup="a = 37")
[0.05412912368774414, 0.05091404914855957, 0.04910898208618164]

>>> timeit.repeat("z=a << 2", setup="a = 37")
[0.04751706123352051, 0.04259490966796875, 0.041903018951416016]

>>> timeit.repeat("z=a >> 2", setup="a = 37")
[0.04719185829162598, 0.04201006889343262, 0.042105913162231445]
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.