Коллеги, сегодня мне захотелось показать вам образчик замечательного кода, в котором нарушаются все требования «правильного программирования» и который может вызвать сердечный приступ у мисрастов, структурастов и прочих ревнителей образцового кода (если Вы из «этих», дальше лучше не читайте. Или читайте, но под свою ответственность).
Итак, имеем такую задачу. Написать функцию, которая принимает указатель на массив байтов, длину этого массива (гарантированно больше нуля) и должна побайтово, байт за байтом, что-нибудь сделать с этим массивом (например отправить в порт вывода).
Ну, первое же решение (из первых глав КиР) могло бы выглядеть, например, так (пока будем отправлять байты в сериал, чтобы посмотреть как оно работает):
void normalLoop(const uint8_t *from, size_t count) {
do Serial.write(*from++); while (--count > 0);
}
Ненуачё, функция как функция, всё с нею в порядке.
Но, «армянское радио», как вы понимаете, такими простыми вещами не занимается, а потому, давайте задумаемся. ну, хорошо, при маленьких массивах данных тут всё нормально. А при больших? Если данных там кило(или мега)байты? А мы на каждый байтик делаем декремент и проверку. Не накладны ли накладные расходы?
Вот, хорошо бы, если бы знать, что count
, например, кратно 8 или там 16. Мы могли бы развернуть в линию 8(16) выводов и делать в 8(16) раз меньше декрементов и сравнений!
void d8Loop(const uint8_t *from, size_t count) {
count = count / 8;
do {
Serial.write(*from++);
Serial.write(*from++);
Serial.write(*from++);
Serial.write(*from++);
Serial.write(*from++);
Serial.write(*from++);
Serial.write(*from++);
Serial.write(*from++);
} while (--count > 0);
}
Но, оно ж, зараза, не кратно! По крайней мере, никто кратности не гарантирует! Чё делать-то?
Не, ну, можно, конечно, сделать два цикла. Сначала по байтику вывести остаток от деления count
на 8 байтов, а потом блоками по 8 выводить уже кратное восьми количество.
Но как же по-лоховски это выглядит с двумя циклами!
void lochLoop(const uint8_t *from, size_t count) {
size_t remainder = count % 8;
for (size_t remainder = count % 8; remainder > 0; Serial.write(*from++), remainder--);
count = count / 8;
do {
Serial.write(*from++);
Serial.write(*from++);
Serial.write(*from++);
Serial.write(*from++);
Serial.write(*from++);
Serial.write(*from++);
Serial.write(*from++);
Serial.write(*from++);
} while (--count > 0);
}
А нельзя ли как-нибудь … можно! В этом языке и не такое можно!
Знакомьтесь!
void duffsDevice(const uint8_t *from, size_t count) {
size_t n = (count + 7) / 8;
switch (count % 8) {
case 0:
do {
Serial.write(*from++);
case 7: Serial.write(*from++);
case 6:Serial.write(*from++);
case 5:Serial.write(*from++);
case 4:Serial.write(*from++);
case 3:Serial.write(*from++);
case 2:Serial.write(*from++);
case 1:Serial.write(*from++);
} while(--n > 0);
}
}
Обратите внимание, do
начинается с case
0. А вот все остальные case
находятся уже внутри do
(а почему бы и нет? Они ж просто метки!).
Получается, что если длина буфера кратна 8 (остаток от деления равен 0), мы сразу же входим в do
и выводим все байты восьмёрками, как мы хотели.
Если же длина не кратна восьми, то мы влазим в цикл do
мимо его заголовка, сразу на case
, соответствующий остатку. Т.е. первый проход нашего do
выводит не восьмёрку байтов, а “сколько надо” (а надо столько, чему равен остаток от деления), а все последующие проходы уже выводят восьмёрки байтов!
Ну, разве не прелесть? Как писал поэт: «Как обаятельно для тех, кто понимает!»
Оно, конечно, компилятор испредупреждается весь про case
без break
, ну да и Бог с ним – так и было задумано. Мы же не лохи какие, а нормальные пацаны! Для таких как мы Страдивари барабаны делал. Что нам до предупреждений какого-то компилятора?! Мы здесь альфа-самцы или он?
Убедимся, что оно нормально работает на буферах с длиной кратной и не кратной восьми:
Вот такой код
void duffsDevice(const uint8_t *from, size_t count) {
size_t n = (count + 7) / 8;
switch (count % 8) {
case 0:
do {
Serial.write(*from++);
case 7: Serial.write(*from++);
case 6:Serial.write(*from++);
case 5:Serial.write(*from++);
case 4:Serial.write(*from++);
case 3:Serial.write(*from++);
case 2:Serial.write(*from++);
case 1:Serial.write(*from++);
} while(--n > 0);
}
}
const uint8_t buffer1[] = "Ninety percent of everything is crud.";
const uint8_t buffer2[] = "12345678";
const uint8_t buffer3[] = "1234567887654321";
const uint8_t buffer4[] = "1234567887654";
//
constexpr size_t bufLength1 = sizeof(buffer1) - 1;
constexpr size_t bufLength2 = sizeof(buffer2) - 1;
constexpr size_t bufLength3 = sizeof(buffer3) - 1;
constexpr size_t bufLength4 = sizeof(buffer4) - 1;
void setup(void) {
Serial.begin(9600);
Serial.println("Friday's shitcode. Brilliant Duff's Device!\r\n");
duffsDevice(buffer2, bufLength2);
Serial.println();
duffsDevice(buffer3, bufLength3);
Serial.println();
duffsDevice(buffer4, bufLength4);
Serial.println();
duffsDevice(buffer1, bufLength1);
Serial.println();
}
void loop(void) {}
даёт вот такой результат
Friday's shitcode. Brilliant Duff's Device!
12345678
1234567887654321
1234567887654
Ninety percent of everything is crud.
Вроде, всё нормально!
Теперь давайте возьмём буфер побольше (ну, килобайт) и сравним скорость первого, пришедшего в голову решения и нашего «говнокода». Для того, чтобы медленный сериал не влиял на результат, будем не выводить в сериал, а писать байт в PORTC
. Ничего порту С
не сделается.
Вот такой код
void normalLoop(const uint8_t *from, size_t count) {
do PORTC = *from++; while (--count > 0);
}
void duffsDevice(const uint8_t *from, size_t count) {
size_t n = (count + 7) / 8;
switch (count % 8){
case 0:
do {
PORTC = *from++;
case 7: PORTC = *from++;
case 6: PORTC = *from++;
case 5: PORTC = *from++;
case 4: PORTC = *from++;
case 3: PORTC = *from++;
case 2: PORTC = *from++;
case 1: PORTC = *from++;
} while(--n > 0);
}
}
constexpr size_t bufLength = 1024;
static uint8_t buffer[bufLength];
uint32_t testIt(void (* f)(const uint8_t *, size_t)) {
const uint32_t start = micros();
f (buffer, bufLength);
return micros() - start;
}
void setup(void) {
Serial.begin(9600);
Serial.println("Friday's shitcode. Brilliant Duff's Device!");
//
// Заполним буфер всякой фигнёй
//
for (size_t i = 0; i < bufLength; i++) buffer[i] = random(256);
//
// Замерим время исполнения наших функций
//
const uint32_t normalTime = testIt(normalLoop);
const uint32_t duffsTime = testIt(duffsDevice);
Serial.print("normalLoop: ");
Serial.print(normalTime);
Serial.println(" uS");
Serial.print("duffsDevice: ");
Serial.print(duffsTime);
Serial.println(" uS");
}
void loop(void) {}
даёт вот такой результат
Friday's shitcode. Brilliant Duff's Device!
normalLoop: 580 uS
duffsDevice: 244 uS
Как видим, выигрыш по скорости, действительно существенный.
Этот приём придумал Том Дафф (Tom Duff) в 1983 году. Он тогда писал: «Если никто не придумал такого до меня, то я бы назвал этот приём своим именем – Duff’s device». Не знаю, придумал ли кто-нибудь такую прелесть до Тома, знаю только, что я – точно не придумал. Завидно