Як працює пристрій Даффа?


Відповіді:


240

В іншому місці є кілька хороших пояснень, але дозвольте спробувати. (Це набагато простіше на дошці!) Ось приклад Вікіпедії з деякими позначеннями.

Скажімо, ви копіюєте 20 байт. Контроль потоку програми для першого проходу є:

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

Тепер, почніть другий прохід, виконуємо лише вказаний код:

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

Тепер почніть третій пропуск:

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

20 байтів тепер скопійовано.

Примітка. Оригінальний пристрій Даффа (показано вище) скопійовано на пристрій вводу / виводу за toадресою. Таким чином, збільшувати покажчик не потрібно *to. При копіюванні між двома буферами пам'яті, які вам потрібно буде використовувати *to++.


1
Як можна пропустити випадок 0: пункт та продовжити перевірку інших пунктів, які знаходяться всередині циклу do while, що є аргументом пропущеного пункту? Якщо пропущений єдиний пункт, який знаходиться за межами do, коли цикл пропущений, чому комутатор не закінчується?
Аврелій

14
Не дивіться на брекети так сильно. Не дивись doтак багато. Натомість подивіться на switchта whileяк на старомодні обчислені висловлювання GOTOчи jmpзаяви асемблера зі зміщенням. switchРобить деяку математику , а потім jmpS в потрібне місце. whileРобить булеву перевірку , а потім наосліп jmpS направо про те, де doбув.
Клінтон Пірс

Якщо це так добре, чому не всі використовують це? Чи є якісь недоліки?
AlphaGoku

@AlphaGoku Читання.
ЛФ

108

Пояснення в журналі доктора Доббі це краще , що я знайшов на цю тему.

Це мій момент AHA:

for (i = 0; i < len; ++i) {
    HAL_IO_PORT = *pSource++;
}

стає:

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

стає:

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

хороший пост (плюс я маю знайти одну хорошу відповідь від вас, щоб підняти;) 2 вниз, 13 - і далі: stackoverflow.com/questions/359727#486543 ). Насолоджуйтесь значком приємної відповіді.
VonC

13
Найважливіший факт тут, і який зробив пристрій Даффа незрозумілим для мене протягом найдовшого часу, полягає в тому, що за химерністю C, коли вперше він досягає часу, він стрибає назад і виконує всі заяви. Таким чином, навіть якщо len%8було 4, він виконає випадок 4, випадок 2, випадок 2 і випадок 1, а потім відскочить назад і виконає всі справи з наступного циклу далі. Це та частина, яка потребує пояснення, спосіб взаємодії циклу та вимикача.
ShreevatsaR

2
Стаття доктора Доббса хороша, однак окрім посилання відповідь нічого не додає. Дивіться відповідь Роб Кеннеді нижче, яка насправді дає важливий момент щодо залишку оброблюваного розміру передачі, а за ним - нульового або більше блоків передачі з 8 байтів. На мою думку, це ключ до розуміння цього коду.
Річард Чемберс

3
Я щось пропускаю, або у другому коді фрагменти len % 8байтів не будуть скопійовані?
новачок

Я застряг, забувши, що якщо ви не напишете заяву про перерву в кінці списку заяв справи, C (або будь-яка інша мова) піде на виконання заяв. Тож якщо вам цікаво, чому пристрій
Даффа

75

У пристрою Даффа є дві ключові речі. По-перше, я підозрюю, що це легше зрозуміти, цикл розкручується. Це торгує більшим розміром коду для більшої швидкості, уникаючи деяких накладних витрат, які перевіряють, чи закінчено цикл і стрибають назад до вершини циклу. Процесор може працювати швидше, коли виконує прямий код замість стрибків.

Другий аспект - оператор перемикання. Це дозволяє коду вперше стрибати в середину циклу. Дивовижна частина більшості людей полягає в тому, що таке дозволяється. Ну, це дозволено. Виконання починається з обчисленої мітки регістру, а потім проникає до кожного наступного оператора призначення, як і будь-який інший оператор перемикання. Після останнього мітки випадку виконання досягає нижньої частини циклу, після чого він стрибає наверх. Верхня частина циклу знаходиться всередині оператора перемикання, тому перемикач більше не оцінюється.

Оригінальний цикл розкручується вісім разів, тому кількість ітерацій ділиться на вісім. Якщо кількість байтів, які потрібно скопіювати, не кратна восьми, то залишилося кілька байтів. Більшість алгоритмів, які копіюють блоки байтів за один раз, будуть обробляти залишки байтів в кінці, але пристрій Даффа обробляє їх на початку. Функція обчислює count % 8для оператора перемикання, щоб зрозуміти, що буде залишок, переходить до мітки справи на стільки байтів і копіює їх. Потім цикл продовжує копіювати групи з восьми байтів.


5
Це пояснення має більше сенсу. Ключовим для мене є розуміння того, що залишок копіюється спочатку, а потім решта в блоки по 8 байт, що незвично, оскільки, як згадувалося більшу частину часу, ви будете копіювати в блоки по 8 байт, а потім копіювати залишок. перше виконання останнього є ключем до розуміння цього алгоритму.
Річард Чемберс

+1 для згадки про шалене розміщення / вкладення перемикача / циклу. Неможливо уявити, що походить з такої мови, як Java ...
Паробай

13

Сенс пристрою дахів полягає в тому, щоб зменшити кількість порівнянь, здійснених в умовах жорсткої метч-реалізації.

Припустимо, ви хочете скопіювати "підрахунок" байтів від a до b, прямий підхід полягає в наступному:

  do {                      
      *a = *b++;            
  } while (--count > 0);

Скільки разів вам потрібно порівняти підрахунок, щоб побачити, чи є вище 0? 'рахувати' разів.

Тепер пристрій "Дафф" використовує неприємний ненавмисний побічний ефект корпусу комутатора, який дозволяє зменшити кількість порівнянь, необхідних для підрахунку / 8.

Тепер припустимо, що ви хочете скопіювати 20 байт за допомогою пристрою duff, скільки порівнянь вам знадобиться? Лише 3, оскільки ви копіюєте вісім байтів одночасно, окрім останнього першого, куди ви копіюєте лише 4.

ОНОВЛЕНО: Вам не потрібно робити 8 зіставлень / заяв про вимикання, але розумний компроміс між розміром функції та швидкістю.


3
Зауважте, що пристрій Даффа не обмежується 8 дублюваннями в операторі комутатора.
страгер

чому ви не можете просто використовувати замість --count, count = count-8? і використовувати другу петлю для обробки решти?
hhafez

1
Хафефе, ви можете використовувати другу петлю для обробки решти. Але тепер у вас вдвічі більше коду, щоб виконати те саме, без збільшення швидкості.
Роб Кеннеді

Йохане, ти маєш це назад. Решта 4 байти копіюються на першій ітерації циклу, а не на останню.
Роб Кеннеді

8

Коли я прочитав його вперше, я автоматично відформатував це

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

і я поняття не мав, що відбувається.

Можливо, не тоді, коли це питання було задано, але зараз у Вікіпедії є дуже хороше пояснення

Пристрій дійсний, легальний C в силу двох атрибутів на С:

  • Розслаблена специфікація оператора перемикання у визначенні мови. На момент винаходу пристрою це було перше видання мови програмування C, яке вимагає лише, щоб керований оператор перемикача був синтаксично дійсним (складеним) висловом, в межах якого мітки випадків можуть з'являтися з префіксацією будь-якого підзакладу. У поєднанні з тим, що за відсутності заяви про перерву потік керування буде проходити з оператора, керованого однією міткою випадку, до цього, керованого наступним, це означає, що код визначає послідовність підрахунку копій з послідовні адреси джерел до виведеного на пам'ять порту виводу.
  • Здатність легально стрибати в середину петлі в С.

6

1: Пристрій Дафф - це особливе розгортання циклу. Що таке розмотування циклу?
Якщо у вас є операція з виконання N разів у циклі, ви можете торгувати розміром програми для швидкості, виконуючи цикл N / n разів, а потім у циклі вбудовуючи (розкручуючи) код циклу n разів, наприклад, замінюючи:

for (int i=0; i<N; i++) {
    // [The loop code...] 
}

з

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 - немає необхідності в Даффі! Якщо це неправда, тоді вам доведеться впоратися з рештою - що є болем.

2: Чим відрізняється пристрій Даффа від цієї стандартної петлі?
Пристрій Duffs - це просто розумний спосіб вирішення циклів, що залишилися, коли N% n! = 0. Весь до / час виконує N / n кількість разів, як у стандартній розмотці циклу (тому що застосовується випадок 0). Під час останнього пробігу через цикл ('N / n + 1'-й раз) починається випадок, і ми переходимо до N% n випадку і запускаємо код циклу кількість разів' залишок '.


Мене зацікавив пристрій Даффа наступним цим питанням: stackoverflow.com/questions/17192246/switch-case-weird-scoping, тому я подумав, що я маю намір уточнити Даффа - не впевнений, чи є якесь поліпшення існуючих відповідей ...
Ricibob

3

Хоча я не на 100% впевнений, що ти просиш, тут іде ...

Проблема, що стосується пристроїв Даффа, полягає в розкручуванні циклу (як ви, без сумніву, бачили на опублікованому посиланням Wiki). Це, в основному, прирівнюється до оптимізації ефективності роботи за часом пам’яті пам’яті. Пристрій Даффа стосується послідовного копіювання, а не просто будь-якої старої проблеми, але є класичним прикладом того, як можна здійснити оптимізацію, зменшивши кількість разів, яку потрібно проводити порівняння в циклі.

В якості альтернативного прикладу, який може полегшити розуміння, уявіть, що у вас є масив елементів, які ви хочете переосмислити, і додайте до них 1 раз ... як правило, ви можете використовувати цикл та циклічно близько 100 разів . Це здається досить логічним, і це ... однак, оптимізацію можна зробити, відкрутивши цикл (очевидно, не надто далеко ... або ви також можете просто не використовувати цикл).

Отже, звичайна для циклу:

for(int i = 0; i < 100; i++)
{
    myArray[i] += 1;
}

стає

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

Що робить пристрій Даффа, це реалізувати цю ідею в C, але (як ви бачили у Вікі) із серійними копіями. Те, що ви бачите вище, на прикладі розмотування, - це 10 порівнянь порівняно зі 100 в оригіналі - це означає незначну, але можливо значну оптимізацію.


8
Вам не вистачає ключової частини. Йдеться не лише про розмотування циклу. Оператор перемикача стрибає в середину циклу. Саме це і робить пристрій таким заплутаним. Ваш цикл вище завжди виконує кратну кількість 10 копій, але Дафф виконує будь-яке число.
Роб Кеннеді

2
Це правда - але я намагався спростити опис ОП. Можливо, я цього не зрозумів достатньо! :)
Джеймс Б

2

Ось не детальне пояснення, що я вважаю сутністю пристрою Даффа:

Справа в тому, що C - це в основному приємний фасад для мови складання (складання PDP-7 має бути специфічним; якби ви вивчали, ви б побачили, наскільки яскраві подібності). І, мовою складання, у вас насправді немає циклів - у вас є мітки та вказівки із умовно-розгалуженням. Отже цикл - це лише частина загальної послідовності інструкцій з міткою та гілкою десь:

        instruction
label1: instruction
        instruction
        instruction
        instruction
        jump to label1  some condition

і інструкція перемикання дещо розгалужується / стрибає вперед:

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

У монтажі легко уявити, як поєднувати ці дві структури управління, і коли ви думаєте про це таким чином, їх комбінація в C вже не здається такою дивною.


1

Це відповідь, яку я опублікував на інше запитання щодо пристрою Даффа, який отримав кілька перетворень до того, як питання було закрито як дублікат. Я думаю, що це дає дещо цінний контекст щодо того, чому слід уникати цієї конструкції.

"Це пристрій Даффа . Це метод розгортання циклів, який дозволяє уникнути необхідності додавати вторинний цикл виправлення для розгляду випадків, коли кількість ітерацій циклу не може бути точним кратним фактором розгортання.

Оскільки більшість відповідей тут здаються загалом позитивними, я хочу висвітлити мінуси.

За допомогою цього коду компілятор буде намагатися застосувати будь-яку оптимізацію до тіла циклу. Якщо ви тільки що написали код як простий цикл, сучасний компілятор повинен мати можливість обробляти розгортання для вас. Таким чином ви зберігаєте читабельність та продуктивність та сподіваєтесь на інші оптимізації, застосовані до тіла циклу.

Стаття у Вікіпедії, на яку посилаються інші, навіть говорить про те, коли цю «модель» було знято з вихідного коду Xfree86.

Цей результат типовий для сліпої оптимізації будь-якого коду, який, на вашу думку, може знадобитися. Це заважає компілятору виконувати свою роботу належним чином, робить ваш код менш читабельним і більш схильним до помилок і, як правило, уповільнює його. Якби ви робили речі в першу чергу правильно, тобто писали простий код, потім профілювали вузькі місця, потім оптимізували, ви ніколи не думали використовувати щось подібне. Не з сучасним процесором та компілятором.

Це чудово це зрозуміти, але я буду здивований, якщо ви коли-небудь насправді ним користуєтесь ".


0

Тільки експериментуючи, знайшов ще один варіант, що уживається без переплетення перемикача та циклу:

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

Де ваша умова припинення?
користувач2338150
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.