Я прочитав статтю у Вікіпедії на пристрої Даффа , і не розумію. Мені дуже цікаво, але я прочитав там пояснення пару разів, і досі не розумію, як працює пристрій Даффа.
Що було б більш детальним поясненням?
Я прочитав статтю у Вікіпедії на пристрої Даффа , і не розумію. Мені дуже цікаво, але я прочитав там пояснення пару разів, і досі не розумію, як працює пристрій Даффа.
Що було б більш детальним поясненням?
Відповіді:
В іншому місці є кілька хороших пояснень, але дозвольте спробувати. (Це набагато простіше на дошці!) Ось приклад Вікіпедії з деякими позначеннями.
Скажімо, ви копіюєте 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++
.
do
так багато. Натомість подивіться на switch
та while
як на старомодні обчислені висловлювання GOTO
чи jmp
заяви асемблера зі зміщенням. switch
Робить деяку математику , а потім jmp
S в потрібне місце. while
Робить булеву перевірку , а потім наосліп jmp
S направо про те, де do
був.
Пояснення в журналі доктора Доббі це краще , що я знайшов на цю тему.
Це мій момент 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);
}
len%8
було 4, він виконає випадок 4, випадок 2, випадок 2 і випадок 1, а потім відскочить назад і виконає всі справи з наступного циклу далі. Це та частина, яка потребує пояснення, спосіб взаємодії циклу та вимикача.
len % 8
байтів не будуть скопійовані?
У пристрою Даффа є дві ключові речі. По-перше, я підозрюю, що це легше зрозуміти, цикл розкручується. Це торгує більшим розміром коду для більшої швидкості, уникаючи деяких накладних витрат, які перевіряють, чи закінчено цикл і стрибають назад до вершини циклу. Процесор може працювати швидше, коли виконує прямий код замість стрибків.
Другий аспект - оператор перемикання. Це дозволяє коду вперше стрибати в середину циклу. Дивовижна частина більшості людей полягає в тому, що таке дозволяється. Ну, це дозволено. Виконання починається з обчисленої мітки регістру, а потім проникає до кожного наступного оператора призначення, як і будь-який інший оператор перемикання. Після останнього мітки випадку виконання досягає нижньої частини циклу, після чого він стрибає наверх. Верхня частина циклу знаходиться всередині оператора перемикання, тому перемикач більше не оцінюється.
Оригінальний цикл розкручується вісім разів, тому кількість ітерацій ділиться на вісім. Якщо кількість байтів, які потрібно скопіювати, не кратна восьми, то залишилося кілька байтів. Більшість алгоритмів, які копіюють блоки байтів за один раз, будуть обробляти залишки байтів в кінці, але пристрій Даффа обробляє їх на початку. Функція обчислює count % 8
для оператора перемикання, щоб зрозуміти, що буде залишок, переходить до мітки справи на стільки байтів і копіює їх. Потім цикл продовжує копіювати групи з восьми байтів.
Сенс пристрою дахів полягає в тому, щоб зменшити кількість порівнянь, здійснених в умовах жорсткої метч-реалізації.
Припустимо, ви хочете скопіювати "підрахунок" байтів від a до b, прямий підхід полягає в наступному:
do {
*a = *b++;
} while (--count > 0);
Скільки разів вам потрібно порівняти підрахунок, щоб побачити, чи є вище 0? 'рахувати' разів.
Тепер пристрій "Дафф" використовує неприємний ненавмисний побічний ефект корпусу комутатора, який дозволяє зменшити кількість порівнянь, необхідних для підрахунку / 8.
Тепер припустимо, що ви хочете скопіювати 20 байт за допомогою пристрою duff, скільки порівнянь вам знадобиться? Лише 3, оскільки ви копіюєте вісім байтів одночасно, окрім останнього першого, куди ви копіюєте лише 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, яке вимагає лише, щоб керований оператор перемикача був синтаксично дійсним (складеним) висловом, в межах якого мітки випадків можуть з'являтися з префіксацією будь-якого підзакладу. У поєднанні з тим, що за відсутності заяви про перерву потік керування буде проходити з оператора, керованого однією міткою випадку, до цього, керованого наступним, це означає, що код визначає послідовність підрахунку копій з послідовні адреси джерел до виведеного на пам'ять порту виводу.
- Здатність легально стрибати в середину петлі в С.
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 випадку і запускаємо код циклу кількість разів' залишок '.
Хоча я не на 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 в оригіналі - це означає незначну, але можливо значну оптимізацію.
Ось не детальне пояснення, що я вважаю сутністю пристрою Даффа:
Справа в тому, що 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 вже не здається такою дивною.
Це відповідь, яку я опублікував на інше запитання щодо пристрою Даффа, який отримав кілька перетворень до того, як питання було закрито як дублікат. Я думаю, що це дає дещо цінний контекст щодо того, чому слід уникати цієї конструкції.
"Це пристрій Даффа . Це метод розгортання циклів, який дозволяє уникнути необхідності додавати вторинний цикл виправлення для розгляду випадків, коли кількість ітерацій циклу не може бути точним кратним фактором розгортання.
Оскільки більшість відповідей тут здаються загалом позитивними, я хочу висвітлити мінуси.
За допомогою цього коду компілятор буде намагатися застосувати будь-яку оптимізацію до тіла циклу. Якщо ви тільки що написали код як простий цикл, сучасний компілятор повинен мати можливість обробляти розгортання для вас. Таким чином ви зберігаєте читабельність та продуктивність та сподіваєтесь на інші оптимізації, застосовані до тіла циклу.
Стаття у Вікіпедії, на яку посилаються інші, навіть говорить про те, коли цю «модель» було знято з вихідного коду Xfree86.
Цей результат типовий для сліпої оптимізації будь-якого коду, який, на вашу думку, може знадобитися. Це заважає компілятору виконувати свою роботу належним чином, робить ваш код менш читабельним і більш схильним до помилок і, як правило, уповільнює його. Якби ви робили речі в першу чергу правильно, тобто писали простий код, потім профілювали вузькі місця, потім оптимізували, ви ніколи не думали використовувати щось подібне. Не з сучасним процесором та компілятором.
Це чудово це зрозуміти, але я буду здивований, якщо ви коли-небудь насправді ним користуєтесь ".
Тільки експериментуючи, знайшов ще один варіант, що уживається без переплетення перемикача та циклу:
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;
}