Duff'ın cihazında Wikipedia'daki makaleyi okudum ve anlamıyorum. Gerçekten ilgileniyorum, ancak oradaki açıklamayı birkaç kez okudum ve hala Duff'un cihazının nasıl çalıştığını anlamıyorum.
Daha ayrıntılı bir açıklama ne olabilir?
Duff'ın cihazında Wikipedia'daki makaleyi okudum ve anlamıyorum. Gerçekten ilgileniyorum, ancak oradaki açıklamayı birkaç kez okudum ve hala Duff'un cihazının nasıl çalıştığını anlamıyorum.
Daha ayrıntılı bir açıklama ne olabilir?
Yanıtlar:
Başka bir yerde bazı iyi açıklamalar var, ama bir deneyeyim. (Bu bir beyaz tahtada çok daha kolaydır!) İşte bazı gösterimlerin olduğu Wikipedia örneği.
20 bayt kopyaladığınızı varsayalım. İlk geçiş için programın akış kontrolü:
int count; // Set to 20
{
int n = (count + 7) / 8; // n is now 3. (The "while" is going
// to be run three times.)
switch (count % 8) { // The remainder is 4 (20 modulo 8) so
// jump to the case 4
case 0: // [skipped]
do { // [skipped]
*to = *from++; // [skipped]
case 7: *to = *from++; // [skipped]
case 6: *to = *from++; // [skipped]
case 5: *to = *from++; // [skipped]
case 4: *to = *from++; // Start here. Copy 1 byte (total 1)
case 3: *to = *from++; // Copy 1 byte (total 2)
case 2: *to = *from++; // Copy 1 byte (total 3)
case 1: *to = *from++; // Copy 1 byte (total 4)
} while (--n > 0); // N = 3 Reduce N by 1, then jump up
// to the "do" if it's still
} // greater than 0 (and it is)
}
Şimdi, ikinci geçişe başlayın, sadece belirtilen kodu çalıştırıyoruz:
int count; //
{
int n = (count + 7) / 8; //
//
switch (count % 8) { //
//
case 0: //
do { // The while jumps to here.
*to = *from++; // Copy 1 byte (total 5)
case 7: *to = *from++; // Copy 1 byte (total 6)
case 6: *to = *from++; // Copy 1 byte (total 7)
case 5: *to = *from++; // Copy 1 byte (total 8)
case 4: *to = *from++; // Copy 1 byte (total 9)
case 3: *to = *from++; // Copy 1 byte (total 10)
case 2: *to = *from++; // Copy 1 byte (total 11)
case 1: *to = *from++; // Copy 1 byte (total 12)
} while (--n > 0); // N = 2 Reduce N by 1, then jump up
// to the "do" if it's still
} // greater than 0 (and it is)
}
Şimdi üçüncü geçişe başlayın:
int count; //
{
int n = (count + 7) / 8; //
//
switch (count % 8) { //
//
case 0: //
do { // The while jumps to here.
*to = *from++; // Copy 1 byte (total 13)
case 7: *to = *from++; // Copy 1 byte (total 14)
case 6: *to = *from++; // Copy 1 byte (total 15)
case 5: *to = *from++; // Copy 1 byte (total 16)
case 4: *to = *from++; // Copy 1 byte (total 17)
case 3: *to = *from++; // Copy 1 byte (total 18)
case 2: *to = *from++; // Copy 1 byte (total 19)
case 1: *to = *from++; // Copy 1 byte (total 20)
} while (--n > 0); // N = 1 Reduce N by 1, then jump up
// to the "do" if it's still
} // greater than 0 (and it's not, so bail)
} // continue here...
Şimdi 20 bayt kopyalandı.
Not: Orijinal Duff'ın Cihazı (yukarıda gösterilen) to
adresteki bir G / Ç cihazına kopyalandı . Bu nedenle, işaretçiyi artırmak gerekli değildi *to
. İki bellek arabelleği arasında kopyalama yaparken kullanmanız gerekir *to++
.
do
Çok fazla bakma . Bunun yerine, bakmak switch
ve while
eski moda bilgisayarlı olarak GOTO
statments veya montajcı jmp
bir ofset ile ifadeleri. switch
Sonra bazı matematik yapar ve jmp
doğru yere s. while
Bir boolean kontrol yapan ve daha sonra körlemesine jmp
nerede sağa s do
idi.
Dr Dobb'un Journal açıklama ı konu üzerinde bulunan en iyisidir.
Bu benim AHA anım:
for (i = 0; i < len; ++i) {
HAL_IO_PORT = *pSource++;
}
şu hale gelir:
int n = len / 8;
for (i = 0; i < n; ++i) {
HAL_IO_PORT = *pSource++;
HAL_IO_PORT = *pSource++;
HAL_IO_PORT = *pSource++;
HAL_IO_PORT = *pSource++;
HAL_IO_PORT = *pSource++;
HAL_IO_PORT = *pSource++;
HAL_IO_PORT = *pSource++;
HAL_IO_PORT = *pSource++;
}
n = len % 8;
for (i = 0; i < n; ++i) {
HAL_IO_PORT = *pSource++;
}
şu hale gelir:
int n = (len + 8 - 1) / 8;
switch (len % 8) {
case 0: do { HAL_IO_PORT = *pSource++;
case 7: HAL_IO_PORT = *pSource++;
case 6: HAL_IO_PORT = *pSource++;
case 5: HAL_IO_PORT = *pSource++;
case 4: HAL_IO_PORT = *pSource++;
case 3: HAL_IO_PORT = *pSource++;
case 2: HAL_IO_PORT = *pSource++;
case 1: HAL_IO_PORT = *pSource++;
} while (--n > 0);
}
len%8
, 4 olsa bile , 4 numaralı durumu, 2 numaralı durumu ve 1 numaralı durumu çalıştıracak ve ardından geri atlayıp sonraki döngüden itibaren tüm vakaları yürütecektir . Bu, açıklanması gereken kısımdır, döngü ve switch deyiminin "etkileşim" yolu.
len % 8
bayt kopyalanmayacak mı?
Duff'un cihazında iki önemli şey var. İlk olarak, anlaşılması daha kolay olan kısım olduğundan şüphelendiğim, döngü açıldı. Bu, döngünün bitip bitmediğini kontrol etmek ve döngünün tepesine geri atlamakla ilgili ek yüklerin bir kısmını önleyerek daha fazla hız için daha büyük kod boyutu ticareti yapar. CPU, atlama yerine düz çizgi kodu yürütürken daha hızlı çalışabilir.
İkinci yön, anahtar ifadesidir. Kodun ilk kez döngünün ortasına atlamasına izin verir . Çoğu insan için şaşırtıcı olan kısım, böyle bir şeye izin verilmesidir. İzin verilir. Yürütme, hesaplanan durum etiketinde başlar ve daha sonra , diğer herhangi bir switch deyimi gibi, her bir ardışık atama deyimine düşer . Son durum etiketinden sonra, yürütme döngünün altına ulaşır ve bu noktada en üste geri döner. Döngünün üst kısmı switch ifadesinin içindedir , bu nedenle anahtar artık yeniden değerlendirilmez.
Orijinal döngü sekiz kez çözülür, bu nedenle yineleme sayısı sekize bölünür. Kopyalanacak bayt sayısı sekizin katı değilse, kalan bazı baytlar vardır. Bir seferde bayt bloklarını kopyalayan çoğu algoritma, kalan baytları sonunda işleyecektir, ancak Duff'un cihazı bunları başlangıçta ele alır. İşlev count % 8
, kalan kısmın ne olacağını hesaplamak için switch deyimini hesaplar , bu bayt için büyük / küçük harf etiketine atlar ve bunları kopyalar. Ardından döngü sekiz baytlık grupları kopyalamaya devam eder.
Duffs cihazının amacı, sıkı bir memcpy uygulamasında yapılan karşılaştırma sayısını azaltmaktır.
A'dan b'ye 'sayım' baytlarını kopyalamak istediğinizi varsayalım, basit yaklaşım aşağıdakileri yapmaktır:
do {
*a = *b++;
} while (--count > 0);
0'ın üzerinde olup olmadığını görmek için sayımı kaç kez karşılaştırmanız gerekir? 'sayma' kez.
Şimdi, duff cihazı, saymak için gereken karşılaştırma sayısını / 8 azaltmanıza olanak tanıyan bir anahtar kasasının kötü bir istenmeyen yan etkisini kullanıyor.
Şimdi, duffs cihazını kullanarak 20 bayt kopyalamak istediğinizi varsayalım, kaç tane karşılaştırmaya ihtiyacınız olur? Yalnızca 3, çünkü yalnızca 4 tane kopyaladığınız sonuncu bayt dışında bir seferde sekiz bayt kopyaladığınız için.
GÜNCELLENDİ: 8 karşılaştırma / case-in-switch deyimi yapmanız gerekmez, ancak işlev boyutu ve hız arasında makul bir denge vardır.
İlk kez okuduğumda, buna otomatik olarak biçimlendirdim
void dsend(char* to, char* from, count) {
int n = (count + 7) / 8;
switch (count % 8) {
case 0: do {
*to = *from++;
case 7: *to = *from++;
case 6: *to = *from++;
case 5: *to = *from++;
case 4: *to = *from++;
case 3: *to = *from++;
case 2: *to = *from++;
case 1: *to = *from++;
} while (--n > 0);
}
}
ve ne olduğu hakkında hiçbir fikrim yoktu.
Belki bu soru sorulduğunda değil, ama şimdi Wikipedia'nın çok iyi bir açıklaması var
Cihaz, C'deki iki özellik nedeniyle geçerlidir, yasal C'dir:
- Dilin tanımında switch ifadesinin rahat belirtimi. Cihazın icadı sırasında, bu, yalnızca anahtarın kontrollü ifadesinin sözdizimsel olarak geçerli (bileşik) bir ifade olmasını gerektiren C Programlama Dilinin ilk baskısıydı; bu durumda, durum etiketleri herhangi bir alt ifadenin önünde görünebilir. Bir break ifadesinin yokluğunda, kontrol akışının bir durum etiketi tarafından kontrol edilen bir ifadeden bir sonrakinin kontrol ettiği ifadeye düşeceği gerçeğiyle bağlantılı olarak, bu, kodun, bellek eşlemeli çıkış bağlantı noktasına sıralı kaynak adresleri.
- C'de yasal olarak bir döngünün ortasına atlama yeteneği.
1: Duffs cihazı, döngü açmanın özel bir uygulamasıdır. Döngü açma, bir döngüde N kez gerçekleştirme işleminiz varsa uygulanabilir bir optimizasyon tekniğidir - döngüyü N / n kez çalıştırarak ve ardından döngüde döngü kodunu n kez sıralarken (açarak) program boyutunu hız için değiştirebilirsiniz. değiştirme:
for (int i=0; i<N; i++) {
// [The loop code...]
}
ile
for (int i=0; i<N/n; i++) {
// [The loop code...]
// [The loop code...]
// [The loop code...]
...
// [The loop code...] // n times!
}
N% n == 0 ise bu harika çalışıyor - Duff'a gerek yok! Bu doğru değilse, geri kalanı halletmeniz gerekir - ki bu bir acıdır.
2: Duffs cihazının bu standart döngü açmadan farkı nedir?
Duffs cihazı, N% n! = 0 olduğunda kalan döngü döngüleri ile başa çıkmanın akıllıca bir yoludur. Tüm do / while, standart döngü açmaya göre N / n defa çalıştırır (çünkü 0 durumu geçerlidir). On son 'normal' döngü çalıştırmak yoluyla kalan çalışır - ve durum tekmeler döngü aracılığıyla ilk çalıştırma biz döngü kodunu kez 'kalan' numarasını çalıştırın.
Ne istediğinizden% 100 emin olmasam da, işte burada ...
Duff'un cihazının adreslediği sorun, döngü çözme sorunlarından biridir (şüphesiz gönderdiğiniz Wiki bağlantısında görmüş olacağınız gibi). Bunun temelde eşit olduğu şey, bellek ayak izi üzerinde çalışma zamanı verimliliğinin optimizasyonudur. Duff'un cihazı, herhangi bir eski problemden ziyade seri kopyalamayla ilgilenir, ancak bir döngüde bir karşılaştırmanın yapılması gerekenlerin sayısını azaltarak optimizasyonların nasıl yapılabileceğinin klasik bir örneğidir.
Anlaşılmasını kolaylaştırabilecek alternatif bir örnek olarak, döngü yapmak istediğiniz bir dizi öğe olduğunu hayal edin ve her seferinde bunlara 1 ekleyin ... normalde bir for döngüsü kullanabilir ve yaklaşık 100 kez döngü yapabilirsiniz. . Bu oldukça mantıklı görünüyor ve öyle ... Ancak, döngüyü çözerek bir optimizasyon yapılabilir (tabii ki çok uzak değil ... ya da sadece döngüyü kullanmayabilirsiniz).
Yani normal bir for döngüsü:
for(int i = 0; i < 100; i++)
{
myArray[i] += 1;
}
olur
for(int i = 0; i < 100; i+10)
{
myArray[i] += 1;
myArray[i+1] += 1;
myArray[i+2] += 1;
myArray[i+3] += 1;
myArray[i+4] += 1;
myArray[i+5] += 1;
myArray[i+6] += 1;
myArray[i+7] += 1;
myArray[i+8] += 1;
myArray[i+9] += 1;
}
Duff'un cihazının yaptığı şey, bu fikri C'de uygulamaktır, ancak (Wiki'de gördüğünüz gibi) seri kopyalarla. Çözülmemiş örnekle yukarıda gördüğünüz şey, orijinalde 100 ile karşılaştırıldığında 10 karşılaştırmadır - bu, küçük ama muhtemelen önemli bir optimizasyon anlamına gelir.
İşte Duff'un cihazının özü olduğunu düşündüğüm ayrıntılı olmayan bir açıklama:
Mesele şu ki, C temelde montaj dili için güzel bir cephedir (PDP-7 montajı spesifiktir; eğer çalışırsanız benzerliklerin ne kadar çarpıcı olduğunu göreceksiniz). Ve montaj dilinde, gerçekten döngüleriniz yok - etiketleriniz ve koşullu dal talimatlarınız var. Dolayısıyla döngü, bir etiket ve bir yerde bir dal içeren genel talimat dizisinin sadece bir parçasıdır:
instruction
label1: instruction
instruction
instruction
instruction
jump to label1 some condition
ve bir anahtar talimatı bir şekilde dallanma / atlamadır:
evaluate expression into register r
compare r with first case value
branch to first case label if equal
compare r with second case value
branch to second case label if equal
etc....
first_case_label:
instruction
instruction
second_case_label:
instruction
instruction
etc...
Montajda, bu iki kontrol yapısının nasıl birleştirileceği kolayca düşünülebilir ve bu şekilde düşündüğünüzde, C'deki kombinasyonları artık o kadar tuhaf görünmüyor.
Bu, Duff'un Cihazı ile ilgili, soru yinelenen olarak kapatılmadan önce bazı olumlu tepkiler alan başka bir soruya gönderdiğim bir cevap. Bence bu yapıdan neden kaçınmanız gerektiğine dair burada biraz değerli bir bağlam sağlıyor.
"Bu, Duff'ın Cihazıdır. Döngü yineleme sayısının açılma faktörünün tam katı olduğunun bilinmediği zamanlarla başa çıkmak için ikincil bir düzeltme döngüsü eklemek zorunda kalmadan döngüleri açma yöntemidir.
Buradaki yanıtların çoğu bu konuda genel olarak olumlu göründüğünden, olumsuz yönlerini vurgulayacağım.
Bu kodla bir derleyici, döngü gövdesine herhangi bir optimizasyonu uygulamak için mücadele edecektir. Kodu basit bir döngü olarak yazdıysanız, modern bir derleyici sizin için açma işlemini halledebilmelidir. Bu şekilde okunabilirliği ve performansı sürdürürsünüz ve döngü gövdesine başka optimizasyonların uygulanmasını umarsınız.
Başkaları tarafından atıfta bulunulan Wikipedia makalesi, bu 'kalıp' Xfree86 kaynak kodundan çıkarıldığında performansın gerçekten arttığını söylüyor.
Bu sonuç, ihtiyaç duyabileceğini düşündüğünüz herhangi bir kodu körü körüne el ile optimize etmenin tipik bir sonucudur. Derleyicinin işini düzgün şekilde yapmasını engeller, kodunuzu daha az okunabilir hale getirir ve hatalara daha yatkın hale getirir ve tipik olarak yavaşlatır. İşleri ilk başta doğru şekilde yapıyor olsaydınız, yani basit kod yazıyor, ardından darboğazlar için profil oluşturuyorsanız, sonra optimizasyon yapıyorsanız, böyle bir şeyi kullanmayı asla düşünmezdiniz. Zaten modern bir CPU ve derleyici ile değil.
Anlamak güzel, ama gerçekten kullanırsan şaşırırım. "
İşte Duff's Device ile 64-bit memcpy için çalışan bir örnek:
#include <iostream>
#include <memory>
inline void __memcpy(void* to, const void* from, size_t count)
{
size_t numIter = (count + 56) / 64; // gives the number of iterations; bit shift actually, not division
size_t rest = count & 63; // % 64
size_t rest7 = rest&7;
rest -= rest7;
// Duff's device with zero case handled:
switch (rest)
{
case 0: if (count < 8)
break;
do { *(((unsigned long long*&)to)++) = *(((unsigned long long*&)from)++);
case 56: *(((unsigned long long*&)to)++) = *(((unsigned long long*&)from)++);
case 48: *(((unsigned long long*&)to)++) = *(((unsigned long long*&)from)++);
case 40: *(((unsigned long long*&)to)++) = *(((unsigned long long*&)from)++);
case 32: *(((unsigned long long*&)to)++) = *(((unsigned long long*&)from)++);
case 24: *(((unsigned long long*&)to)++) = *(((unsigned long long*&)from)++);
case 16: *(((unsigned long long*&)to)++) = *(((unsigned long long*&)from)++);
case 8: *(((unsigned long long*&)to)++) = *(((unsigned long long*&)from)++);
} while (--numIter > 0);
}
switch (rest7)
{
case 7: *(((unsigned char*)to)+6) = *(((unsigned char*)from)+6);
case 6: *(((unsigned short*)to)+2) = *(((unsigned short*)from)+2); goto case4;
case 5: *(((unsigned char*)to)+4) = *(((unsigned char*)from)+4);
case 4: case4: *((unsigned long*)to) = *((unsigned long*)from); break;
case 3: *(((unsigned char*)to)+2) = *(((unsigned char*)from)+2);
case 2: *((unsigned short*)to) = *((unsigned short*)from); break;
case 1: *((unsigned char*)to) = *((unsigned char*)from);
}
}
void main()
{
static const size_t NUM = 1024;
std::unique_ptr<char[]> str1(new char[NUM+1]);
std::unique_ptr<char[]> str2(new char[NUM+1]);
for (size_t i = 0 ; i < NUM ; ++ i)
{
size_t idx = (i % 62);
if (idx < 26)
str1[i] = 'a' + idx;
else
if (idx < 52)
str1[i] = 'A' + idx - 26;
else
str1[i] = '0' + idx - 52;
}
for (size_t i = 0 ; i < NUM ; ++ i)
{
memset(str2.get(), ' ', NUM);
__memcpy(str2.get(), str1.get(), i);
if (memcmp(str1.get(), str2.get(), i) || str2[i] != ' ')
{
std::cout << "Test failed for i=" << i;
}
}
return;
}
Sıfır uzunluktaki durumu ele alır (orijinal Duff Cihazında sayı> 0 varsayımı vardır). Main () işlevi, __memcpy için basit test durumlarını içerir.
Sadece deneyerek, switch
ifadeyi ve do
- while
döngüsünü karıştırmadan iyi geçinen başka bir varyant bulundu :
int n = (count + 1) / 8;
switch (count % 8)
{
LOOP:
case 0:
if(n-- == 0)
break;
putchar('.');
case 7:
putchar('.');
case 6:
putchar('.');
case 5:
putchar('.');
case 4:
putchar('.');
case 3:
putchar('.');
case 2:
putchar('.');
case 1:
putchar('.');
default:
goto LOOP;
}
Teknik olarak, goto
hala bir döngü uygular, ancak bu değişken biraz daha okunabilir olabilir.
switch
ve do {} while
garip ve muhtemelen anlaşılması zor görünen. İfadeler çok kesin değildir, çünkü goto
yapı hala teknik olarak bir döngü uygular, sadece daha okunabilir bir varyantı bulmayı amaçlamaktadır.