Rust'un 128 bitlik tamsayısı "i128" 64 bitlik bir sistemde nasıl çalışır?


128

Rust, 128 bitlik tam sayılara sahiptir, bunlar veri türüyle gösterilir i128(ve u128işaretsiz girişler için):

let a: i128 = 170141183460469231731687303715884105727;

Rust bu i128değerleri 64 bitlik bir sistemde nasıl çalıştırır; örneğin bunlar üzerinde nasıl aritmetik yapar?

Bildiğim kadarıyla, değer bir x86-64 CPU'nun bir yazmacına sığamayacağından, derleyici bir şekilde bir i128değer için 2 yazmaç kullanıyor mu? Yoksa bunun yerine onları temsil etmek için bir tür büyük tamsayı yapısı mı kullanıyorlar?



54
Sadece 10 parmağınız varken iki basamaklı bir tam sayı nasıl çalışır?
Jörg W Mittag

27
@JorgWMittag: Ah - eski "sadece on parmakla iki basamaklı sayı" hilesi. Heh heh. Beni o eskiyle kandırabileceğini düşündün, ha? Pekala, arkadaşım, herhangi bir ikinci sınıf öğrencisinin söyleyebileceği gibi - ayak parmakları bunun için! ( Peter Sellers'dan iğrenç özür dilerimle ... ve Lady Lytton :-)
Bob Jarvis - Monica'yı

1
FWIW çoğu x86 makinesinde SIMD işlemleri için bazı özel 128 bit veya daha büyük yazmaçlar bulunur. Bkz en.wikipedia.org/wiki/Streaming_SIMD_Extensions Edit: ben bir şekilde Eckes yorumuna @ cevapsız
Ryan1729

4
@ JörgWMittag Nah, bilgisayar bilimcileri, parmakları tek tek indirerek veya uzatarak ikili sayar. Ve şimdi, 132 millet, eve gidiyorum ;-D
Marco13

Yanıtlar:


141

Rust'un tüm tam sayı türleri, LLVM tam sayılarına derlenir . LLVM soyut makinesi, 1'den 2 ^ 23 - 1'e kadar herhangi bir bit genişliğindeki tam sayılara izin verir. * LLVM talimatları genellikle herhangi bir boyuttaki tamsayılar üzerinde çalışır.

Açıkçası, pek çok 8388607 bit mimarisi yoktur, bu nedenle kod yerel makine koduna derlendiğinde, LLVM'nin nasıl uygulanacağına karar vermesi gerekir. Soyut bir talimatın semantiği addLLVM'nin kendisi tarafından tanımlanır. Tipik olarak, yerel kodda tek bir talimat eşdeğeri olan soyut talimatlar, bu yerel talimatla derlenirken, taklit edilmeyenler, muhtemelen birden fazla yerel talimatla birlikte derlenir. mcarton'ın cevabı , LLVM'nin hem yerel hem de benzetilmiş talimatları nasıl derlediğini gösterir.

(Bu, yalnızca yerel makinenin destekleyebileceğinden daha büyük tamsayılar için değil, aynı zamanda daha küçük olanlar için de geçerlidir. Örneğin, modern mimariler yerel 8 bit aritmetiği desteklemeyebilir, bu nedenle addiki i8s ile ilgili bir talimat taklit edilebilir daha geniş bir talimatla fazladan bitler atılır.)

Derleyici bir şekilde bir i128değer için 2 kayıt kullanıyor mu? Yoksa onları temsil etmek için bir tür büyük tamsayı yapısı mı kullanıyorlar?

LLVM IR seviyesinde cevap ne: i128diğer tüm tek değerli türler gibi tek bir sicile sığar . Öte yandan, makine koduna çevrildikten sonra ikisi arasında gerçekten bir fark yoktur, çünkü yapılar tıpkı tamsayılar gibi kayıtlara ayrıştırılabilir. Aritmetik yaparken, LLVM'nin her şeyi iki kayda yükleyeceği oldukça güvenli bir bahis.


* Ancak, tüm LLVM arka uçları eşit oluşturulmamıştır. Bu cevap x86-64 ile ilgilidir. 128'den büyük boyutlar ve ikisinin gücü olmayanlar için arka uç desteğinin sivilceli olduğunu anlıyorum (bu, Rust'un neden yalnızca 8-, 16-, 32-, 64- ve 128-bit tam sayıları gösterdiğini kısmen açıklayabilir). Reddit'teki est31'e göre, rustc, onları yerel olarak desteklemeyen bir arka ucu hedeflerken yazılımda 128 bit tamsayılar uygular.


1
Ha, ben neden acaba yerine daha tipik 2 ^ 32 (iyi, bu numaralar görünme sıklığını açısından genel olarak konuşursak, değil derleyici backendleri ... desteklediği tamsayılar maksimum bit genişlikleri açısından) 2 ^ 23
Fonun Monica's Lawsuit

26
@NicHartley LLVM'nin bazı alt sınıflarının, alt sınıfların verileri depolayabileceği bir alan vardır. İçin Typesınıf, bu araçlar tür Çeşidi ne saklamak için 8 bit (fonksiyon, blok, tamsayı, ...) ve alt sınıf veri için 24 bit vardır. IntegerTypeSınıf daha sonra örneklerini düzgünce 32 bit yeterlidir sağlayan boyutunu depolamak için bu 24 bit kullanır!
Todd Sewell

56

Derleyici bunları birden çok yazmaçta saklar ve gerekirse bu değerler üzerinde aritmetik yapmak için birden çok talimat kullanır. Çoğu ISA'lar gibi bir eklenti ile taşıması talimat var x86 yıllardanadc oldukça verimli ekleme / alt tamsayı genişletilmiş hassas yapmak kolaylaştırır.

Örneğin, verilen

fn main() {
    let a = 42u128;
    let b = a + 1337;
}

derleyici, x86-64 için optimizasyon olmadan derlerken aşağıdakileri üretir:
(yorumlar @PeterCordes tarafından eklenir)

playground::main:
    sub rsp, 56
    mov qword ptr [rsp + 32], 0
    mov qword ptr [rsp + 24], 42         # store 128-bit 0:42 on the stack
                                         # little-endian = low half at lower address

    mov rax, qword ptr [rsp + 24]
    mov rcx, qword ptr [rsp + 32]        # reload it to registers

    add rax, 1337                        # add 1337 to the low half
    adc rcx, 0                           # propagate carry to the high half. 1337u128 >> 64 = 0

    setb    dl                           # save carry-out (setb is an alias for setc)
    mov rsi, rax
    test    dl, 1                        # check carry-out (to detect overflow)
    mov qword ptr [rsp + 16], rax        # store the low half result
    mov qword ptr [rsp + 8], rsi         # store another copy of the low half
    mov qword ptr [rsp], rcx             # store the high half
                             # These are temporary copies of the halves; probably the high half at lower address isn't intentional
    jne .LBB8_2                       # jump if 128-bit add overflowed (to another not-shown block of code after the ret, I think)

    mov rax, qword ptr [rsp + 16]
    mov qword ptr [rsp + 40], rax     # copy low half to RSP+40
    mov rcx, qword ptr [rsp]
    mov qword ptr [rsp + 48], rcx     # copy high half to RSP+48
                  # This is the actual b, in normal little-endian order, forming a u128 at RSP+40
    add rsp, 56
    ret                               # with retval in EAX/RAX = low half result

değerin ve 42içinde depolandığını görebilirsiniz .raxrcx

(editörün notu: x86-64 C çağrı kuralları RDX: RAX'te 128 bitlik tamsayılar döndürür. Ancak bu mainhiç bir değer döndürmez. Tüm fazladan kopyalama tamamen optimizasyonu devre dışı bırakmaktan kaynaklanır ve Rust aslında hata ayıklamada taşma olup olmadığını kontrol eder modu.)

Karşılaştırma için, x86-64'teki Rust 64-bit tamsayılar için asm, burada taşıma ile eklentiye gerek yoktur, her değer için sadece tek bir kayıt veya yığın yuvası.

playground::main:
    sub rsp, 24
    mov qword ptr [rsp + 8], 42           # store
    mov rax, qword ptr [rsp + 8]          # reload
    add rax, 1337                         # add
    setb    cl
    test    cl, 1                         # check for carry-out (overflow)
    mov qword ptr [rsp], rax              # store the result
    jne .LBB8_2                           # branch on non-zero carry-out

    mov rax, qword ptr [rsp]              # reload the result
    mov qword ptr [rsp + 16], rax         # and copy it (to b)
    add rsp, 24
    ret

.LBB8_2:
    call panic function because of integer overflow

Setb / test hala tamamen gereksizdir: jc(CF = 1 ise zıpla) gayet iyi çalışır.

Optimizasyon etkinleştirildiğinde, Rust derleyicisi taşma olup olmadığını kontrol etmez, bu nedenle böyle +çalışır .wrapping_add().


4
@Anush Hayır, rax / rsp / ... 64 bitlik kayıtlardır. Her 128 bitlik sayı, iki kayıt / bellek konumunda saklanır ve bu da iki 64 bitlik eklemeyle sonuçlanır.
ManfP

5
@Anush: hayır, optimizasyon devre dışı bırakılarak derlendiği için çok fazla talimat kullanıyor. Sen görürdünüz çok iki aldı bir işlev derlenmiş if (sadece ekleme / adc gibi) daha basit bir kod u128(böyle bir değeri args ve döndürülen godbolt.org/z/6JBza0 ) yapmaktan derleyici durdurmak için yerine optimizasyon durdurmaktansa, derleme zamanı sabiti bağımsız değişkenler üzerinde sabit yayılma.
Peter Cordes

3
@ CAD97 Yayın modu , sarma aritmetiğini kullanır ancak hata ayıklama modunun yaptığı gibi taşma ve paniği kontrol etmez. Bu davranış, RFC 560 tarafından tanımlanmıştır . UB değil.
trentcl

3
@PeterCordes: Özellikle, Rust dili taşmanın belirtilmediğini belirtir ve rustc (tek derleyici) seçim için iki davranış belirtir: Panik veya Sarma. İdeal olarak, Panik varsayılan olarak kullanılır. Pratikte, optimalin altında kod üretimi nedeniyle, Yayın modunda varsayılan değer Wrap'tır ve uzun vadeli bir hedef, (eğer varsa) kod üretimi genel kullanım için "yeterince iyi" olduğunda Panik'e geçmektir. Ayrıca, tüm Rust integral türleri, bir davranışı seçmek için adlandırılmış işlemleri destekler: işaretli, sarma, doyurma, ... böylece seçilen davranışı işlem başına geçersiz kılabilirsiniz.
Matthieu M.

1
@MatthieuM .: Evet, ilkel türlerdeki sarmalamaya karşı işaretli ve doyurucu ekleme / alt / kaydırma / her ne yöntem olursa olsun seviyorum. C'nin imzasız paketlemesinden çok daha iyi olan UB, sizi buna göre seçmeye zorluyor. Her neyse, bazı ISA'lar Panik için etkili destek sağlayabilir, örneğin bir dizi işlemden sonra kontrol edebileceğiniz yapışkan bir bayrak. (0 veya 1 ile üzerine yazılan x86'nın OF veya CF'sinden farklı olarak), örneğin Agner Fog'un önerdiği ForwardCom ISA ( agner.org/optimize/blog/read.php?i=421#478 ) Ancak bu, optimizasyonu hiçbir zaman hesaplama yapmamak için yine de kısıtlıyor Rust kaynağı yapmadı. : /
Peter Cordes

30

Evet, 32-bit makinelerde 64-bit tam sayıların veya 16-bit makinelerde 32-bit tam sayıların, hatta 8-bit makinelerde 16-ve 32-bit tam sayıların işlendiği şekilde (mikrodenetleyiciler için hala geçerlidir! ). Evet, numarayı iki kütüğe veya hafıza konumuna ya da her neyse (gerçekten önemli değil) saklarsınız. Toplama ve çıkarma önemsizdir, iki talimat alır ve taşıma işaretini kullanır. Çarpma, üç çarpma ve bazı eklemeler gerektirir (64-bit yongaların halihazırda iki kayda çıktı veren 64x64-> 128 bir çarpma işlemine sahip olması yaygındır). Bölme ... bir alt rutin gerektirir ve oldukça yavaştır (bir sabite bölünmenin bir vardiya veya çarpmaya dönüştürülebildiği bazı durumlar hariç), ancak yine de çalışır. Bitsel ve / veya / x veya sadece üst ve alt yarılar için ayrı ayrı yapılmalıdır. Vardiya, rotasyon ve maskeleme ile gerçekleştirilebilir. Ve bu hemen hemen her şeyi kapsar.


26

Belki daha net bir örnek sağlamak için, x86_64'te -Obayrakla derlenen

pub fn leet(a : i128) -> i128 {
    a + 1337
}

derler

example::leet:
  mov rdx, rsi
  mov rax, rdi
  add rax, 1337
  adc rdx, 0
  ret

(My orijinal yayın vardı u128ziyadei128 sorduğunuzdan . İşlev her iki şekilde de aynı kodu derler, imzalı ve işaretsiz eklemenin modern bir CPU'da aynı olduğunu gösteren iyi bir gösteri.)

Diğer liste optimize edilmemiş kod üretti. Bir hata ayıklayıcıda adım atmak güvenlidir, çünkü herhangi bir yere bir kesme noktası koyabilmenizi ve programın herhangi bir satırındaki herhangi bir değişkenin durumunu inceleyebilmenizi sağlar. Okuması daha yavaş ve daha zor. Optimize edilmiş sürüm, aslında üretimde çalışacak olan koda çok daha yakındır.

aBu fonksiyonun parametresi bir çift 64-bit yazmaç, rsi: rdi'de geçirilir. Sonuç, başka bir kayıt çiftinde döndürülür, rdx: rax. Kodun ilk iki satırı, toplamı şu şekilde başlatır:a .

Üçüncü satır, girişin düşük kelimesine 1337 ekler. Bu taşarsa, CPU'nun taşıma bayrağında 1'i taşır. Dördüncü satır, girişin yüksek kelimesine sıfır ekler - artı taşındıysa 1'i.

Bunu iki basamaklı bir sayıya tek basamaklı bir sayının basit bir şekilde eklenmesi olarak düşünebilirsiniz.

  a  b
+ 0  7
______
 

ancak 18,446,744,073,709,551,616 tabanında. Hala ilk önce en düşük "rakamı" ekliyorsunuz, muhtemelen bir sonraki sütuna 1 taşıyıp, ardından bir sonraki rakamı artı elde ekliyorsunuz. Çıkarma çok benzer.

Çarpma, (2⁶⁴a + b) (2⁶⁴c + d) = 2¹²⁸ac + 2⁶⁴ (ad + bc) + bd kimliğini kullanmalıdır; burada bu çarpımların her biri, bir kayıtta ürünün üst yarısını ve ürünün alt yarısını bir diğeri. Bu terimlerden bazıları atlanacak, çünkü 128'in üzerindeki bitler biru128 ve atılır. Öyle olsa bile, bu birkaç makine talimatı gerektirir. Bölünme ayrıca birkaç adım atar. İşaretli bir değer için, çarpma ve bölme işleminin ek olarak işlenenlerin işaretlerini ve sonucu dönüştürmesi gerekecektir. Bu operasyonlar hiç de verimli değil.

Diğer mimarilerde işler daha kolay veya zorlaşır. RISC-V, 128 bitlik bir komut seti uzantısını tanımlar, ancak bildiğim kadarıyla kimse onu silikonda uygulamamıştır. Bu uzantı olmadan , RISC-V mimari el kitabı bir koşullu dalı önerir :addi t0, t1, +imm; blt t0, t1, overflow

SPARC, x86'nın kontrol bayrakları gibi kontrol kodlarına sahiptir, ancak add,ccbunları ayarlamak için özel bir talimat kullanmanız gerekir. Öte yandan MIPS, iki işaretsiz tamsayının toplamının işlenenlerden kesinlikle daha az olup olmadığını kontrol etmenizi gerektirir. Eğer öyleyse, ekleme aşıldı. En azından, koşullu dallanma olmadan taşıma bitinin değerine başka bir kayıt koyabilirsiniz.


1
son paragraf: Bir sonucun yüksek bitine bakarak iki işaretsiz sayıdan hangisinin daha büyük olduğunu tespit etmek için, bit girişleri için subbir n+1bit alt sonucuna ihtiyacınız vardır n. yani, aynı genişlikte sonucun işaret bitine değil, yürütmeye bakmanız gerekir. Bu nedenle x86 işaretsiz dallanma koşulları SF'ye (bit 63 veya 31) değil, CF'ye (tam mantıksal sonucun bit 64 veya 32'si) dayanır.
Peter Cordes

1
re: divmod: AArch64'ün yaklaşımı, bölme ve tamsayı yapan bir talimat sağlamak x - (a*b), kalan kısmı temettü, bölüm ve bölenden hesaplamaktır. (Bu, bölme kısmı için çarpımsal bir tersi kullanan sabit bölenler için bile kullanışlıdır). Div + mod komutlarını tek bir divmod işleminde birleştiren ISA'ları okumamıştım; bu harika.
Peter Cordes

1
re: flags: evet, bir bayrak çıkışı, OoO exec + register-yeniden adlandırma işleminin bir şekilde işlemesi gereken 2. bir çıktıdır. x86 CPU'lar, FLAGS değerinin temel aldığı tamsayı sonucuna sahip birkaç ekstra biti tutarak bunun üstesinden gelir, bu nedenle muhtemelen ZF, SF ve PF gerektiğinde anında üretilir. Sanırım bununla ilgili bir Intel patenti var. Bu, ayrı ayrı izlenmesi gereken çıktıların sayısını 1'e düşürür. (Intel CPU'larda, hiçbir uop 1'den fazla tamsayı yazmaç mul r64yazamaz ; örneğin 2 uops, ikincisi RDX yüksek yarısını yazıyor).
Peter Cordes

1
Ancak verimli genişletilmiş hassasiyet için bayraklar çok iyidir. Temel sorun olmadan Superscalar olarak sipariş yürütülmesi için yazmaç yeniden adlandırma. bayraklar bir WAW tehlikesidir (yazdıktan sonra yazın). Elbette, taşıma ile ekleme talimatları 3 girişlidir ve bu aynı zamanda izlenecek önemli bir sorundur. Intel adc, Broadwell'in kodunu çözmeden önce sbbve cmovher biri 2 uop'a. (Haswell, FMA için 3 girişli uop'ları tanıttı, Broadwell bunu tam sayıya genişletti.)
Peter Cordes

1
Bayraklı RISC ISA'lar genellikle bayrak ayarlamayı isteğe bağlı yapar ve fazladan bir bit tarafından kontrol edilir. örneğin ARM ve SPARC bu şekildedir. Her zamanki gibi PowerPC, her şeyi daha karmaşık hale getirir: 8 koşul-kod yazmacına sahiptir (kaydetme / geri yükleme için 32 bitlik bir kayıtta birlikte paketlenmiştir), böylece cc0 veya cc7 veya her neyse karşılaştırabilirsiniz. Ve sonra VE veya VEYA koşul kodları birlikte! Dal ve cmov talimatları hangi CR kaydının okunacağını seçebilir. Yani bu size aynı anda x86 ADCX / ADOX gibi birden fazla bayrak açma zincirine sahip olmanızı sağlar. alanclements.org/power%20pc.html
Peter Cordes
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.