Те, кто интересуется системными языками вроде C
и C++
обычно рано или поздно натыкаются на так называемый
“Метод Даффа” (Duff’s Device). Его идея в общем заключается в ускорении работы циклов
при помощи трюка, который, вероятно, сильно удивит пользователей современных языков программирования.
Такой трюк достаточно нормален для программистов на ассемблере, и ещё один лишний пример того, что на low-level уровне программирования можно выжать из железа больше.
Попробую объяснить максимально просто.
Я немного упростил код, чтобы не отвлекаться на особенности синтаксиса C
.
С “нормальной” версией можно ознакомиться на википедии
Допустим, нам нужен цикл, чтобы повторить (например, копирование значений куда-то) определённую операцию n-ное число раз. Типичная реализация такого цикла будет выглядеть примерно так:
register count = 123; /* любая цифра числа итераций, но больше 0 */
do {
/* КАКАЯ-ТО ОПЕРАЦИЯ; */;
} while (--count > 0);
Для большинства программистов с этим кодом нет никаких проблем, и на самом деле так и есть. Но что если нам не хватает производительности и хочется итерироваться быстрее?
Дело в том, что операция сравнения, определяющая условие цикла, в терминах машинного кода довольно “медленная” и для ускорения проводить её следует как можно реже. Как это сделать? Можно увеличить количество операций в теле цикла, скажем, до 8 (цифра 8 особо ничем не обусловлена, это число может быть как больше, так и меньше):
register count = 32;
register n = count / 8;
do {
/* ОПЕРАЦИЯ; */
/* ОПЕРАЦИЯ; */
/* ОПЕРАЦИЯ; */
/* ОПЕРАЦИЯ; */
/* ОПЕРАЦИЯ; */
/* ОПЕРАЦИЯ; */
/* ОПЕРАЦИЯ; */
/* ОПЕРАЦИЯ; */
} while (--n > 0);
Такой код будет проверять условие цикла while
в 8 раз реже, что даст выигрыш в производительности по сравнению с предыдущим,
однако возникает проблема: что если число итераций не кратно 8? Как быть с остатком?
Тут то нам и пригодится Метода Даффа (сама техника изобретена не Даффом, и была известна до этого ассемблер-программистам, но
ему принадлежит это выражение на языке C
):
register count = 19;
register n = (count + 7) / 8;
switch (count % 8) {
case 0: do { /* ОПЕРАЦИЯ; */
case 7: /* ОПЕРАЦИЯ; */
case 6: /* ОПЕРАЦИЯ; */
case 5: /* ОПЕРАЦИЯ; */
case 4: /* ОПЕРАЦИЯ; */
case 3: /* ОПЕРАЦИЯ; */
case 2: /* ОПЕРАЦИЯ; */
case 1: /* ОПЕРАЦИЯ; */
} while (--n > 0);
}
Выглядит страшно, но попробуем разобраться.
Да, кусок конструкции switch-case
находится прямо внутри цикла, причём не целиком (case 0:
остался снаружи).
Тут оказывается, что синтаксис C
не запрещает нам “прыгнуть” к любой инструкции в теле цикла, например с помощью goto
(так как цикл представляет собой набор инструкций с jmp
-условием, которое проверяет, следует ли его продолжать).
Теперь разберёмся с switch-case
: каждый case
это по сути просто лейбл
(а switch-case
в целом более ограниченный и контролируемый вариант использования goto лебл
).
Еще одна фишка case
в том, что прыгнув на нужный case
и не встретив break
мы проваливаемся дальше по следующим case'ам
, пока конструкция
не закончится или всё-таки не возникнет break
.
Теперь с этим знанием попробуем проследить, как работает эта конструкция для, например, count = 19
:
- переменная
n = (19 + 7) / 8 = 3
(целочисленное деление). - в
switch
:19 % 8 = 3
, переходим кcase 3:
- после
case 3:
нужная операция воспроизведётся 3 раза (это мы сразу разобрались с остатком от деления числа циклов на 8). - далее мы оказываемся в обычном цикле
do-while
,case
нам никак не мешает и ничего больше не делает, и мы спокойно “проваливается” сквозь. - цилк повторится по числу
n
2 раза, что добавит нам8 * 2 = 16
. - Итого: получаем
16 + 3 = 19
выполнений операций, столько сколько нам нужно, и всего за 2 цикла (с кусочком), т.е. всего за 2 проверки условия цикла.
На сегодняшний день метод Даффа используют редко, он смущает компилятор, мешая ему проводить разного рода оптимизации кода (прекрасно понимаю его). В отдельных случаях он также может приводить к замедлению работы кода и увеличению размера исполняемого файла (см. статью на вики) (не забываем также второе правило программирования Роба Пайка). Чуть более “нормальный” вариант этого же подхода - использовать 2 цикла: один для “больших шагов по +/-8 операций за раз” плюс цикл для завершения остатков.
Но что больше всего меня поражает в этой
Потому что это буквально “хак” на уровне языка.
Просто для контраста по сравнению с современными высокоуровневыми языками с их классами, функторами и
виртуальными машинами, примитивная и первобытная сила C
ощущается совсем иначе.
Язык C в каком-то смысле абсолютно “поломанный” по сравнению с тем что ЯП предлагают сегодня, при этом его естественное состояние.
За долгие годы никто (к счастью) кардинально не “““улучшил””” Си, он почти такой же, как и 40 лет назад.
Но, в том числе поэтому нельзя избавиться от ощущения насколько близко находится “реальное железо” при работе с C
, и насколько
по настоящему можно управлять компьютером на нём.