10 Mart 2019 Pazar

Streaming SIMD Extensions (SSE) ve glibc


Merhaba. Bu yazı, önceki kayar noktalı sayılarla ilgili yazıların kamera arkası gibi olacak. Kayar noktalı sayılarla ilgili yazıyı hazırlarken ilginç bir sorun yaşadım. Oracle Blog'daki kodu derleyip assembly çıktısına baktığımda gcc'nin herhangi bir parametre verilmediğinde kayar nokta işlemlerini SSE komutlarıyla derlediğini farkettim. (Platform: Linux Mint 17.3, Centos 6.x ve 7.x)

...
40054d:  f2 0f 10 45 e8        movsd xmm0,QWORD PTR [rbp-0x18]
400552:  bf 18 06 40 00        mov edi,0x400618
400557:  b8 01 00 00 00        mov eax,0x1
40055c:  e8 af fe ff ff        call 400410 <printf@plt>
400561:  f2 0f 10 45 f8        movsd xmm0,QWORD PTR [rbp-0x8]
400566:  f2 0f 10 0d b2 00 00  movsd xmm1,QWORD PTR [rip+0xb2]
                               # 400620 <_IO_stdin_used+0x10>
40056d:  00
40056e:  f2 0f 5e c1           divsd xmm0,xmm1

Yukarıdaki kod parçası printf ve bölme komutlarını içeriyor.

Bu kodu FPU'da çalışmaya zorlamak için önce -mno-sse derleyici parametresini denediğimde aşağıdaki kodu üretti:

...
40054d:  bf 08 06 40 00      mov    edi,0x400608
400552:  b8 00 00 00 00      mov    eax,0x0
400557:  e8 b4 fe ff ff      call    400410 <printf@plt>
40055c:  dd 45 f8            fld QWORD PTR [rbp-0x8]
40055f:  dd 05 ab 00 00 00   fld QWORD PTR [rip+0xab]
                             # 400610 <_IO_stdin_used+0x10>
400565:  de f9               fdivrp st(1),st

Bu aynı kodun assembly çıktısı ilginç biçimde çıktı olarak 4.940656e-324 dışında birşey üretmiyor. Bu arada çıktıyı kısaltmak için double d = 1.0; ifadesini double d = pow(2.0, -1000); olarak değiştirdim. 1075 satır çıktı üreten kod 75 satır çıktı üretmeye başladı.

-mno-sse'li veya -mno-sse'siz çıktının satır sayısı değişmiyor ve uygulama sonlanıyor. Yani sorun hesaplamada değil ama ekrana yanlış yazılıyor. İki kod parçası karşılaştırıldığında, ilkinde sonuç xmm0'a yazılıyor. edi'de "%e\n"in göstericisi, eax'te printf'e giren ekstra argüman sayısı var. İkinci çıktıyla karşılaştırdığımda, -mno-sse yüzünden xmm0 yazmacına erişilemediği için bu komut yok bu nedenle de eax'te 0 var. Bunun C karşılığı printf("%e\n"); ve başka argüman olmadığından muhtemelen xmm0'de kalmış çöp değer ekrana yazılıyor. Bu arada gdb ile kontrol ettiğimde divsd komutu doğru değerlerle çalışıyor. Bu da hatanın printf'te olduğunu ispatlıyor.

Yukarıdaki kod parçalarını Mint'den aldım (glibc v2.19) ama CentOS'da da fark yok. Denemeleri CentOS sanal makinalarda yaptım. Sanırım xmm yazmaçları 64-bit olduklarından, 32-bit kodlarda böyle bir sorun yok. İstenirse SSE komutları 32-bit kodlarda kullanılmak üzere zorlanabiliyor: https://stackoverflow.com/questions/24386541/gcc-wont-use-sse-in-32bit

CentOS 6.x, glibc 2.12 kullanıyor, CentOS 5.6 ve 5.9'la da denediğimde (glibc v2.5) aynı durum tekrarlandı. 64-bitlik ilk CentOS 3.3, glibc v2.3.2 kullanıyor. Bununla denediğimde sanırım gcc (v3.2.3) sürümündeki bir hatadan ötürü -mno-sse'nin bir etkisi olmadı. Kod çıktısında sonuç yine xmm0'da printf'e gidiyor gibiydi.

Öyle anlaşılıyor ki, glibc derlenirken printf işlevinde SSE kullanılmış ve ondalıklı sayıların printf fonksiyonuna mutlaka xmm0'da girmesi gerekiyor. Bu nedenle -mno-sse parametresi kesinlikle kullanılmamalı. İşlemleri FPU'ya zorlamak için -mno-sse yerine -mfpmath=387 kullanıldığında istenen sonuç elde ediliyor.

Öte yandan önceki yazıda Oracle Blog'da söz edilen -fns parametresinin gcc'de bulunmadığından söz etmiştim. gcc'de aynı işi yapan -ffast-math parametresi bulunuyor. Bu parametre altı ayrı flag'i içeriyor: -fno-math-errno, -funsafe-math-optimizations, -ffinite-math-only, -fno-rounding-math, -fno-signaling-nans ve -fcx-limited-range (man gcc). Bunlardan subnormal sayılar üzerinde etkisi olan -funsafe-math-optimizations parametresi ve bununla derlenen kodda fazladan aşağıdaki kod parçası var:

0000000000400440 <set_fast_math>:
  400440:  0f ae 5c 24 fc        stmxcsr DWORD PTR [rsp-0x4]
  400445:  81 4c 24 fc 40 80 00  or DWORD PTR [rsp-0x4],0x8040
  40044c:  00
  40044d:  0f ae 54 24 fc        ldmxcsr DWORD PTR [rsp-0x4]
  400452:  c3                    ret

MXCSR, SSE komutlarının çalışma şeklini kontrol etmek için kullanılan, FPU Control Word (FCW) benzeri bir yazmaç*. Bu yazmacın 15. ve 6. bitleri yani "Flush to Zero (FZ)" ve "Denormals are Zero (DAZ)" bayraklarına bir veriliyor. Bu parametreyle derlenen kod çalıştırıldığında ekrandaki son çıktı normal double'ların en küçüğü 2.225074e-308 oluyor. MXCSR bir SSE yazmacı olduğundan -ffast-math'in 80387 komutlarına etkisi yok.

80387'de subnormaller için aynı mekanizma bulunmuyor. Önceki yazıda FCW'de subnormallerle ilgili DE biti olduğunu yazmıştım. Bu kullanılarak subnormallerle yapılan işlemler kontrol edilebilir. Kodda FCW'ye erişmek için şu işlevi ekledim:

void setfst()   {
    short FPUCW;
    asm("fstcw %0": "=m" (FPUCW));
    FPUCW = FPUCW & ~0x02;
    asm("fldcw %0":: "m" (FPUCW));
}

Veya diğer seçenek:

void setfst2()   {
    short FPUCW;
    asm("fstcw %0\n\t"
        "and  %1,0xFFFD\n\t"
        "fstcw %1"
        : "=m" (FPUCW): "m" (FPUCW));
}

Bu fonksiyon while döngüsünden hemen önce çağırılmalı ve kod -mfpmath=387 -masm=intel parametreleriyle derlenmeli. Kod çalıştırıldığında en son 2.225074e-308 yazıyor ve ardından "Floating point exception"la sonlanıyor. DE bayrağının elle sıfırlanması subnormal sayılarla yapılan işlemlerin exception oluşturmasına neden oluyor. Bu exception için handler yazılıp; subnormal sayıda yapılan işlem, exception'a neden olduğunda sonuç sıfıra yuvarlanırsa, SSE'deki DAZ bayrağının görevi FPU'ya uyarlanmış olur.

Ek Kaynaklar:
* http://softpixel.com/~cwright/programming/simd/sse.php
* https://software.intel.com/en-us/articles/x87-and-sse-floating-point-assists-in-ia-32-flush-to-zero...