Говнокод по пятницам. Эпизод 4. «Duff's device»

Коллеги, сегодня мне захотелось показать вам образчик замечательного кода, в котором нарушаются все требования «правильного программирования» и который может вызвать сердечный приступ у мисрастов, структурастов и прочих ревнителей образцового кода (если Вы из «этих», дальше лучше не читайте. Или читайте, но под свою ответственность).

Итак, имеем такую задачу. Написать функцию, которая принимает указатель на массив байтов, длину этого массива (гарантированно больше нуля) и должна побайтово, байт за байтом, что-нибудь сделать с этим массивом (например отправить в порт вывода).

Ну, первое же решение (из первых глав КиР) могло бы выглядеть, например, так (пока будем отправлять байты в сериал, чтобы посмотреть как оно работает):

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». Не знаю, придумал ли кто-нибудь такую прелесть до Тома, знаю только, что я – точно не придумал. Завидно :frowning:

3 лайка

Прямо сегодня было :smiley:

Всегда считал, что ℅ настолько дорогая операция, что лучше сто декрементов сделать…

В данном случае “% 8” - просто наложение маски - 1 такт (ну, два, если 16-битное).

А кроме того, она здесь делается ровно один раз, а декрементов и сравнений сильно больше ста.

А в чем подвох? В каком месте эта красота лажает? Иначе это не говнокод…

  • Все-таки желательно, гражданин артист, чтобы вы незамедлительно разоблачили бы перед зрителями технику ваших фокусов. Разоблачение совершенно необходимо. Без этого ваши блестящие номера оставят тягостное впечатление. Зрительская масса требует объяснения.
1 лайк

Говнокод именно потому что требует объяснений. Логика неочевидна при беглом чтении.

Тогда сдаюсь. Что-то в голову не стукнуло, что там степень двойки и оптимизатор почикает всё деление и умножение.

Ну, если для Вас вход в цикл не с начала, а с середины – не говнокод, то и не знаю чем такому эстету угодить :slight_smile:

А говнокод он вовсе не обязан не работать. Некоторые вполне работают десятилетиями.

Для оценки данной “красоты” нужно иметь определение понятия говнокода.

Лично я даже не знаю, стоит ли отнести это к категории говнокода или это должно относиться к отдельной категории, типа “код извращенца”… Ведь оно только выглядит как говнокод, но работает то быстро и правильно.

2 лайка

Он обязан работать (может, не всегда – с граблями, но работать), а если где-то и ломаться, то в неочевидных ситуациях. Это то, что даёт ему право называться кодом. Неработающий “набор символов” – это вообще не код. Никакой, ни “говно-”, ни “конфетно-”.

Это моя рубрика и в ней я буду показывать то, что я считаю таковым. Если у кого-то другое мнение по данному вопросу – нет проблем, пишите свои статьи, открывайте свои рубрики, разве кто-то возражает?

Во! Именно это я и имел в виду

Тут нет сомнений. Но " зрительская масса" тоже может иметь свое мнение :slight_smile:

Нет проблем, у нас свободная страна.

А где смайл?

1 лайк

мне тут недавно высказали, чё ты там за херню повесил, вот тебе плата STM32F407ххх, сделай нормально, ну я и парировал, у этой херни аптайм более 7 лет без сбоев, ни чё так?

Может в “Проекты” уже наконец выложишь? Что там за суперприблуда?

1 лайк

написал код и потерял, не выложу, нету аднака, есть только поделие, моё первое, на esp8266 которое на плате в формате UNO, три датчика температуры льют в базу sql

Так никто и не сомневался, что потерял.

2 лайка

Нету :frowning:

прямо таки и никто…там жеж всё примитивно, три датчика DS18B20, раз в минуту считывают показания, подключаются по вайфаю к серверу и льют в базу…
примитивное всё однако…может потому и не глючит, вай фай на минуту тушится, после передачи

10 балов!
:rofl:

ты хоть “уважаемым людям” успел код выслать?

2 лайка