Bunun için bazı ayrıntılar / arkaplan hakkında yorumlarda (biraz veya tamamen) yanlış tahminler var.
Glibc'in optimize edilmiş C geri dönüşü optimize edilmiş uygulamasına bakıyorsunuz . (Elle yazılmış bir asm uygulaması olmayan ISA'lar için) . Veya bu kodun hala glibc kaynak ağacında bulunan eski bir sürümü. https://code.woboq.org/userspace/glibc/string/strlen.c.html , mevcut glibc git ağacına dayanan bir kod tarayıcıdır. Görünüşe göre, MIPS de dahil olmak üzere birkaç ana glibc hedefi tarafından kullanılıyor. (Teşekkürler @zwol).
X86 ve ARM gibi popüler ISA'larda glibc elle yazılmış bir asm kullanır
Bu nedenle, bu kodla ilgili herhangi bir şeyi değiştirme teşviği düşündüğünüzden daha düşüktür.
Bu bithack kodu ( https://graphics.stanford.edu/~seander/bithacks.html#ZeroInWord ) aslında sunucunuzda / masaüstü / dizüstü bilgisayarınızda / akıllı telefonunuzda çalışan şey değildir. Bir seferde saf bir bayt döngüden daha iyidir, ancak bu bithack bile modern CPU'lar için verimli bir asm ile karşılaştırıldığında oldukça kötüdür (özellikle AVX2 SIMD'nin 32 baytı birkaç talimatla kontrol etmesine izin verir, saatte 32 ila 64 bayt sağlar. 2 / saat vektör yükü ve ALU verimi olan modern CPU'larda L1d önbelleğinde veriler sıcaksa ana döngüde döngü. yani başlangıç ek yükünün baskın olmadığı orta boyutlu dizeler için.)
glibc, CPU'nuz için strlen
en uygun sürüme çözümlemek için dinamik bağlantı hileleri kullanır , bu nedenle x86'da bile bir SSE2 sürümü (16 bayt vektör, x86-64 için taban çizgisi) ve AVX2 sürümü (32 bayt vektör) bulunur.
x86, vektör ve genel amaçlı kayıtlar arasında verimli veri aktarımına sahiptir, bu da döngü kontrolünün verilere bağlı olduğu örtülü uzunluklu dizelerdeki fonksiyonları hızlandırmak için SIMD'nin kullanılmasını benzersiz kılar (?). pcmpeqb
/ pmovmskb
bir seferde 16 ayrı baytın test edilmesini mümkün kılar.
glibc, AdvSIMD kullanarak buna benzer bir AArch64 sürümüne ve vector-> GP kayıtlarının boru hattını durdurduğu AArch64 CPU'lara yönelik bir sürümüne sahiptir, bu nedenle bu bithack'i kullanır . Ancak, bir kez isabet aldığında kayıt içindeki baytı bulmak için sayım önde gelen sıfırları kullanır ve sayfa geçişini kontrol ettikten sonra AArch64'ün verimli hizalanmamış erişiminden yararlanır.
Ayrıca: Optimizasyon etkinken bu kod 6,5x neden daha yavaş? strlen
büyük bir arabellek ve gcc için satır içi bilmek iyi olabilir basit bir asm uygulaması ile x86 asm hızlı ve yavaş neyin hakkında biraz daha ayrıntı var . (Bazı gcc sürümleri rep scasb
akıllıca sıralıdır, ki bu çok yavaştır veya bir seferde 4 baytlık bir bithack. Bu nedenle GCC'nin satır içi stlen tarifi güncellenmesi veya devre dışı bırakılması gerekir.)
Asm'da C stili "tanımlanmamış davranış" yoktur ; bellekteki baytlara istediğiniz gibi erişmek güvenlidir ve geçerli baytları içeren hizalanmış bir yük hata veremez. Bellek koruması hizalanmış sayfa ayrıntı düzeyi ile gerçekleşir; hizalanmış erişim, sayfa sınırını geçemediğinden daha dar. X86 ve x64'te aynı sayfadaki bir arabellek sonuna kadar okumak güvenli midir? Aynı mantık, bu C saldırısının derleyicilerin bu işlevin bağımsız bir satır içi uygulaması için oluşturmalarını sağladığı makine kodu için de geçerlidir.
Derleyici, bilinmeyen satır içi olmayan bir işlevi çağırmak için kod yayınladığında, işlevin herhangi bir genel değişkeni / tüm değişkenleri ve işaretçisi olabilecek herhangi bir belleği değiştirdiğini varsaymalıdır. yani adres çıkışları olmayan yerliler hariç her şey çağrı boyunca bellekte senkronize olmalıdır. Bu açıkça asm ile yazılmış fonksiyonlar için olduğu kadar kütüphane fonksiyonları için de geçerlidir. Bağlantı zamanı optimizasyonunu etkinleştirmezseniz, ayrı çeviri birimleri (kaynak dosyaları) için bile geçerlidir.
Bu neden glibc'nin bir parçası olarak güvenli ama başka türlü değil .
En önemli faktör, bunun strlen
başka hiçbir şeye satır içi olamamasıdır. Bunun için güvenli değil; katı takma UB içerir ( char
an ile veri okuma unsigned long*
). char*
Başka takma şey izin verilir ancak tersi olduğunu değil gerçek .
Bu, önceden derlenmiş bir kitaplık (glibc) için bir kitaplık işlevidir. Arayanlara bağlantı zamanı optimizasyonu ile satır içine alınmaz. Bu, tek başına bir sürümü için güvenli makine kodunu derlemek zorunda olduğu anlamına gelir strlen
. Taşınabilir / güvenli C olması gerekmez.
GNU C kütüphanesi sadece GCC ile derlenmelidir. Görünüşe göre GNU uzantılarını destekleseler bile clang veya ICC ile derlemek desteklenmiyor . GCC, bir C kaynak dosyasını makine kodunun bir nesne dosyasına dönüştüren vaktinden önce derleyicilerdir. Bir tercüman değil, bu yüzden derleme zamanında satır içi gelmedikçe, hafızadaki baytlar sadece hafızadaki baytlardır. başka bir deyişle, farklı türlerdeki erişim, birbirinin içine girmeyen farklı işlevlerde gerçekleştiğinde katı kenar yumuşatma UB tehlikeli değildir.
strlen
Davranışlarının ISO C standardı tarafından tanımlandığını unutmayın . Bu işlev adı özellikle uygulamanın bir parçasıdır . GCC gibi derleyiciler -fno-builtin-strlen
, siz kullanmadıkça adı yerleşik bir işlev olarak görürler , bu nedenle strlen("foo")
derleme zamanı sabiti olabilir 3
. Kütüphanedeki tanım sadece gcc kendi tarifini ya da başka bir şeyi satırlamak yerine ona bir çağrı yapmaya karar verdiğinde kullanılır.
Derleme zamanında UB derleyici tarafından görülmezse , aklı başında makine kodu alırsınız. Makine kod no-UB durum için işe vardır ve hatta eğer istediği için, asm türleri arayan sivri-bellek veri koymak için kullanılan algılamak için bir yolu yoktur.
Glibc, bağlantı zamanı optimizasyonu ile satır içine alınamayan bağımsız bir statik veya dinamik kitaplığa derlenmiştir. glibc'nin yapı komut dosyaları, bir programa satır içi bağlantı kurarken bağlantı zamanı optimizasyonu için makine kodu + gcc içeren GIMPLE dahili temsilini "yağ" statik kitaplıkları oluşturmaz. (yani , ana programa bağlantı zamanı optimizasyonuna libc.a
katılmayacaktır -flto
.) glibc'yi bu şekilde kullanmak, bunu gerçekten kullanan hedeflerde.c
potansiyel olarak güvenli olmayacaktır .
Aslında @zwol'un yorumlarına göre, glibc kaynak dosyaları arasında satırlama yapılması durumunda kırılabilecek "kırılgan" kod nedeniyle LTO, glibc'in kendisini oluştururken kullanılamaz . (Bazı dahili kullanımlar vardır strlen
, örneğin printf
uygulamanın bir parçası olarak )
Bu strlen
bazı varsayımlar yapar:
CHAR_BIT
, 8'in katıdır . Tüm GNU sistemlerinde doğrudur. POSIX 2001 bile garanti veriyor CHAR_BIT == 8
. (Bu CHAR_BIT= 16
veya 32
bazı DSP'lere sahip sistemler için güvenli görünüyor ; hizalanmamış prolog döngüsü sizeof(long) = sizeof(char) = 1
her işaretçi her zaman hizalanmış ve p & sizeof(long)-1
her zaman sıfır olduğu için her zaman 0 yineleme çalıştırır .) Ancak karakterlerin 9 olduğu ASCII olmayan bir karakter kümeniz varsa veya 12 bit genişliğinde, 0x8080...
yanlış desen.
- (belki)
unsigned long
4 veya 8 bayttır. Ya da belki unsigned long
8'e kadar herhangi bir boyut için işe yarayabilir ve bunu assert()
kontrol etmek için bir kullanır .
Bu ikisi mümkün UB değil, sadece bazı C uygulamalarına taşınabilir değiller. Bu kod, çalıştığı platformlarda C uygulamasının bir parçasıdır (veya öyleydi) , bu yüzden iyi.
Bir sonraki varsayım potansiyel C UB'dir:
- Geçerli bayt içeren hizalanmış bir yük hata veremez ve gerçekte istediğiniz nesnenin dışındaki baytları yok saydığınız sürece güvenlidir. (Bellek koruması hizalanmış sayfalık boyu ile her GNU sistemlerinde asm ve tüm normal CPU'lar üzerinde Doğru olur çünkü. O? X86 ve x64 aynı sayfa içinde bir tampon sonu okunmaya güvenli mi UB C güvenli Derleme zamanında görünmez. Inlining olmadan, buradaki durum budur. Derleyici
0
, ilkinden önce okumanın UB olduğunu kanıtlayamaz ; örneğin char[]
içeren bir C dizisi olabilir {1,2,0,3}
)
Bu son nokta, burada bir C nesnesinin sonunu okumayı güvenli kılan şeydir. Mevcut derleyicilerle satır içi oluştururken bile oldukça güvenlidir, çünkü şu anda bir yürütme yolunu ima etmenin ulaşılamaz olduğunu düşünmüyorlar. Ama yine de, bu satır içi izin verirseniz, sıkı takma zaten bir göstericidir.
Sonra, Linux çekirdeğinin işaretçi dökümünü kullanan eski güvensiz memcpy
CPP makrosu gibi sorunlarınız olurdu unsigned long
( gcc, katı takma ve korku hikayeleri ).
Bu strlen
, genel olarak böyle şeylerden kurtulabileceğiniz döneme kadar uzanır ; GCC3'ten önce "sadece inlining olmadığında" uyarı olmadan oldukça güvenli olurdu.
Yalnızca çağrı / ret sınırlarına baktığımızda görünen UB bize zarar veremez. (örneğin, bir bu arama char buf[]
bir dizi üzerinde yerine unsigned long[]
bir döküntünün const char*
). Makine kodu taş haline getirildiğinde, sadece bellekteki baytlarla ilgilenir. Satır içi olmayan bir işlev çağrısı, arayanın herhangi bir / tüm belleği okuduğunu varsaymalıdır.
Sıkı takma UB olmadan bunu güvenle yazma
GCC türü özelliğimay_alias
, A tipi ile aynı ad-şey tedaviyi verir char*
. (@KonradBorowsk tarafından önerilmiştir). GCC başlıkları şu anda bunu __m128i
her zaman güvenle yapabilmeniz için x86 SIMD vektör türleri için kullanıyor _mm_loadu_si128( (__m128i*)foo )
. (Bkz Is `donanım vektör pointer ve ilgili tip tanımlanmamış bir davranış? Arasına reinterpret_cast`ing yapar ve ortalama ne değildir hakkında daha fazla ayrıntı için.)
strlen(const char *char_ptr)
{
typedef unsigned long __attribute__((may_alias)) aliasing_ulong;
aliasing_ulong *longword_ptr = (aliasing_ulong *)char_ptr;
for (;;) {
unsigned long ulong = *longword_ptr++; // can safely alias anything
...
}
}
aligned(1)
İle bir türü ifade etmek için de kullanabilirsiniz alignof(T) = 1
.
typedef unsigned long __attribute__((may_alias, aligned(1))) unaligned_aliasing_ulong;
memcpy
Modern derleyicilerin tek bir yük talimatı olarak nasıl sıralanacağını bildikleri ISO'da bir takma yükü ifade etmenin taşınabilir bir yolu . Örneğin
unsigned long longword;
memcpy(&longword, char_ptr, sizeof(longword));
char_ptr += sizeof(longword);
Bu aynı zamanda hizalanmamış yükler için çalıştığı için memcpy
eserler olarak -eğer tarafından char
-at bir yokmuş erişimi. Ancak pratikte modern derleyiciler memcpy
çok iyi anlıyor .
Buradaki tehlike GCC değilse olduğunu biliyoruz , bu kesin char_ptr
kelime hizalanmış olup, bu asm hizalanmamış yükleri desteklemiyor olabilir bazı platformlarda zaten iç olmayacaktır. örneğin MIPS64r6'dan önceki MIPS veya daha eski ARM. Gerçek bir fonksiyonunuz varsa, memcpy
sadece bir kelime yüklemek (ve onu başka bir bellekte bırakmak) için çağrıda bulunursanız , bu bir felaket olacaktır. GCC bazen kod bir işaretçiyi ne zaman hizaladığını görebilir. Veya uzun bir sınıra ulaşan bir seferlik char döngüsünün ardından
p = __builtin_assume_aligned(p, sizeof(unsigned long));
Bu, nesnenin okunmasını mümkün UB'den kaçınmaz, ancak mevcut GCC ile pratikte tehlikeli değildir.
Neden el ile optimize edilmiş C kaynağı gereklidir: mevcut derleyiciler yeterince iyi değil
Elle optimize edilmiş asm, yaygın olarak kullanılan standart kütüphane işlevi için her son performans düşüşünü istediğinizde daha da iyi olabilir. Özellikle gibi bir şey için memcpy
, ama aynı zamanda strlen
. Bu durumda SSE2'den yararlanmak için C'yi x86 intrinsics ile kullanmak daha kolay olmazdı.
Ancak burada, herhangi bir ISA'ya özgü özellik olmadan saf ve bithack C sürümünden bahsediyoruz.
(Bence strlen
mümkün olduğunca hızlı çalışmasını sağlayacak kadar yaygın olarak kullanılan bir verilen olarak alabiliriz . Bu yüzden soru, daha basit kaynaktan verimli makine kodu alıp alamayacağımızdır. Hayır, yapamayız.)
Mevcut GCC ve clang, yineleme sayısının ilk yinelemeden önce bilinmediği döngüleri otomatik olarak vektörleştiremez . (örneğin , ilk yinelemeyi çalıştırmadan önce döngünün en az 16 yineleme çalıştırıp çalıştırmayacağını kontrol etmek gerekir .) örn. derleyiciler.
Bu, arama döngülerini veya veriye if()break
ve sayaca sahip başka bir döngüyü içerir .
ICC (Intel'in x86 için derleyicisi) bazı arama döngülerini otomatik olarak vektörleştirebilir, ancak yine strlen
de OpenBSD'nin libc kullanımları gibi basit / naif bir C için sadece bir seferde bayt bayramı yapar . ( Godbolt ). ( @ Peske'nin cevabından ).
strlen
Mevcut derleyicilerle performans için elle optimize edilmiş bir libc gereklidir . Bir seferde 1 bayta gitmek (geniş süperskalar CPU'larda döngü başına belki 2 bayt açma) ana bellek, döngü başına yaklaşık 8 bayta kadar ulaşabiliyorsa ve L1d önbellek döngü başına 16 ila 64 verebiliyorsa acınasıdır. (Haswell ve Ryzen'den beri modern ana x86 CPU'larda döngü başına 2x 32 bayt yük. Sadece 512 bit vektörleri kullanmak için saat hızlarını azaltabilen AVX512 sayılmıyor; bu nedenle glibc muhtemelen bir AVX512 sürümü eklemek için acele etmiyor Her ne kadar 256 bit vektörlerle, AVX512VL + BW maskeli bir maske ile karşılaştırılır ve ktest
veya uops / yinelemesini azaltarak daha hiper iş parçacığı kortest
haline getirebilir strlen
.)
Buraya x86 olmayanları dahil ediyorum, bu "16 bayt". Örneğin, çoğu AArch64 işlemcisi en azından bunu yapabilir ve bence kesinlikle daha fazlasını yapabilir. Ve bazılarının strlen
bu yük bant genişliğine ayak uyduracak kadar yürütme kapasitesi vardır .
Tabii ki büyük dizelerle çalışan programlar, örtük uzunluktaki C dizelerinin uzunluğunu çok sık bulmak zorunda kalmamak için genellikle uzunlukları takip etmelidir. Ancak kısa ve orta uzunlukta performans hala elle yazılmış uygulamalardan yararlanır ve eminim bazı programlar orta uzunlukta dizelerde strlen kullanmaktadır.