Duff'un cihazı nasıl çalışır?


Yanıtlar:


245

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) toadresteki 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++.


1
Case 0: cümleciği nasıl atlanabilir ve atlanan cümlenin argümanı olan do while döngüsünün içindeki diğer tümcecikleri kontrol etmeye devam edebilir? Do while döngüsünün dışındaki tek cümle atlanırsa, anahtar neden orada bitmiyor?
Aurelius

16
Diş tellerine bu kadar sert bakma. doÇok fazla bakma . Bunun yerine, bakmak switchve whileeski moda bilgisayarlı olarak GOTOstatments veya montajcı jmpbir ofset ile ifadeleri. switchSonra bazı matematik yapar ve jmpdoğru yere s. whileBir boolean kontrol yapan ve daha sonra körlemesine jmpnerede sağa s doidi.
Clinton Pierce

Bu kadar iyiyse, neden herkes bunu kullanmıyor? Herhangi bir sakınca var mı?
AlphaGoku

1
@AlphaGoku Okunabilirliği.
LF

3
@AlphaGoku modern derleyicileri, döngü açma adı verilen benzer bir şeyi kullanabilir.
Hollay-Horváth Zsombor

111

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);
}

iyi gönderi (artı oy vermek için sizden iyi bir cevap bulmalıyım;) 2 aşağı, gitmek için 13: stackoverflow.com/questions/359727#486543 ). Güzel cevap rozetinin keyfini çıkarın.
VonC

17
Buradaki ve Duff'un cihazını benim için en uzun süre anlaşılmaz kılan en önemli gerçek, C'nin bir tuhaflığı ile, ilk zamana ulaştıktan sonra geri sıçrayıp tüm ifadeleri çalıştırmasıdır. Böylece 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.
ShreevatsaR

3
Dr. Dobbs makalesi iyidir ancak bağlantı dışında yanıt hiçbir şey eklemiyor. İlk önce işlenen transfer boyutunun geri kalanı hakkında önemli bir noktayı ve ardından 8 baytlık sıfır veya daha fazla transfer bloğu sağlayan aşağıdaki Rob Kennedy'nin cevabına bakın. Bence bu kodu anlamanın anahtarı bu.
Richard Chambers

3
Eksik bir şey mi var, yoksa ikinci kod parçasında len % 8bayt kopyalanmayacak mı?
newbie

Bir vakanın ifade listesinin sonuna bir break ifadesi yazmazsanız, C'nin (veya başka bir dilin) ​​ifadeleri yürütmeye devam edeceğini unutmuştum.
Duff'ın

81

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.


7
Bu açıklama daha mantıklı. Kalanların önce sonra geri kalanının 8 baytlık bloklar halinde kopyalandığını anlamamın anahtarı, çünkü çoğu zaman bahsedildiği gibi, 8 baytlık bloklar halinde kopyalayacak ve kalanı kopyalayacaksınız. kalanı önce yapmak, bu algoritmayı anlamanın anahtarıdır.
Richard Chambers

Anahtar / while döngüsünün çılgın yerleşiminden / yuvalanmasından bahsetmek için +1. Java gibi bir dilden geldiğini hayal etmek imkansız ...
Parobay

13

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.


3
Duff'ın cihazının switch ifadesinde 8 tekrarla sınırlı olmadığını unutmayın.
strager

neden --count, count = count-8 yerine kullanamıyorsunuz? ve kalanla başa çıkmak için ikinci bir döngü kullanın?
hhafez

1
Hhafez, kalanla ilgilenmek için ikinci bir döngü kullanabilirsin. Ama şimdi aynı şeyi hız artışı olmadan başarmak için iki kat daha fazla kodunuz var.
Rob Kennedy

Johan, geri almışsın. Kalan 4 bayt , sonuncu değil, döngünün ilk yinelemesinde kopyalanır .
Rob Kennedy

8

İ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.

6

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.


Bu soruyu takiben Duffs cihazına ilgi duydum: stackoverflow.com/questions/17192246/switch-case-weird-scoping, bu yüzden Duff'u açıklığa kavuşturmaya gideceğimi düşündüm - mevcut cevaplarda herhangi bir gelişme olup olmadığından emin değilim ...
Ricibob

@Sjoerd Cheers - düzeltildi.
Ricibob

3

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.


8
Anahtar kısmı kaçırıyorsun. Bu sadece döngü çözme ile ilgili değil. Switch deyimi döngünün ortasına atlar. Cihazın bu kadar kafa karıştırıcı görünmesini sağlayan da budur. Yukarıdaki döngünüz her zaman 10 kopyanın katlarını gerçekleştirir, ancak Duff herhangi bir sayıyı gerçekleştirir.
Rob Kennedy

2
Bu doğru - ancak OP'nin açıklamasını basitleştirmeye çalışıyordum. Belki de bunu yeterince açıklığa kavuşturmadım! :)
James B

2

İş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.


1

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. "


0

İş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.


-1

Sadece deneyerek, switchifadeyi ve do- whiledö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, gotohala bir döngü uygular, ancak bu değişken biraz daha okunabilir olabilir.


Sonlandırma durumunuz nerede?
user2338150

"anahtar ve döngü olmadan" - ve sonra yanıp sönmeden "anahtar () DÖNGÜ: durum 0:" diyerek. Bu nasıl olur da araya girmez?
Sjoerd

@Sjoerd 'Bir döngünün dil yapısı' anlamına geliyordu , yani serpiştirme switchve do {} whilegarip ve muhtemelen anlaşılması zor görünen. İfadeler çok kesin değildir, çünkü gotoyapı hala teknik olarak bir döngü uygular, sadece daha okunabilir bir varyantı bulmayı amaçlamaktadır.
Aconcagua
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.