Тег «C»

Duff’s Device

9 ноября 1983 года Том Дафф изобрел «устройство» имени себя. Вот так оно выглядело:

send(to, from, count)
	register short *to, *from;
	register count;
	{
		register 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);
		}
	}

Такая конструкция предназначалась для раскрутки цикла копирования фрагмента памяти в регистр пословно. Развертывание сокращает количество итераций цикла и, как следствие, количество операций сравнения (в данном случае в 8 раз), тем самым слегка повышая производительность.

Чтобы лучше понять, что происходит, предлагаю читателям запустить следующую программу (я немного изменил изначальный вариант):

#include 
 
int c = 0;
int a = 10;
 
void foo()
{
    printf("%d ", c++);
}
 
void bar()
{
    printf("%d\n", a);
}
 
int main()
{
    switch (a&3) {
    case 0: do { foo(); bar();
    case 3:      foo(); bar();
    case 2:      foo(); bar();
    case 1:      foo(); bar();
	       } while((a -= 4) >= 0);
    }
    return 0;
}

Самое удивительное в этом всем — вовсе не корявое «столбчатое» форматирование. Удивительно то, что язык позволяет размещать цикл внутри блока switch. Интуитивно нам кажется, что каждый case начинает новый блок, и что нельзя соорудить блок, заключающий в себя несколько case’ов. А приведенный выше пример вообще представляется надругательством над синтаксисом C и здравым смыслом. Но стоит задуматься о том, как именно реализована конструкция switch-case, как сразу все становится на свои места:

// исходный код
switch (a) {
    case 0:
        a++;
        break;
    case 1:
        a--;
}
 
// что происходит на самом деле (примерно)
case_0:
    if (a == 1) goto case_1;
    a++;
    goto end;
case_1:
    a--;
end:

Вот так! Теперь понятно, что внутри конструкции switch можно развернуться от души, чем и не преминул воспользоваться Дафф.

Вынужден разочаровать любителей микрооптимизаций: подобное развертывание циклов выполняется современными компиляторами автоматически, и, скорее всего, намного эффективнее. Компилятор ведь может себе позволить не заботиться о чистоте кода. Так что устройство Даффа теперь интересно разве что любителям истории программирования (вроде меня).

Жаждущих подробностей отправляю к оригинальному сообщению Даффа и статье в Википедии.

УжасноПлохоНормальноХорошоОтлично (Еще не оценили)
Loading ... Loading ...

Грабли: замена макросов на функции в C

Произошел со мной недавно один случай. Преамбула: наш отдел разрабатывает ОС реального времени для военных целей, а другой отдел разрабатывает некий софт (неважно, какой именно), который под этой ОС работает. ОС поставляется в виде набора библиотек и заголовочников, а процесс сборки заключается в том, что весь пользовательский софт статически компонуется вместе с ОС в единый монолитный образ, который и загружается на целевую машину. А поскольку все это дело военное, то сопровождается такими захватывающими мероприятиями, как предварительные и государственные испытания, сдача программного продукта в архив и т.д. Кто в ящиках работал, тот знает.

Теперь собственно случай. Понадобилось мне поменять кое-какой код в libc. Гляжу в заголовочный файл — а там некоторые функции и не функции вовсе, а макросы. Ну, думаю, непорядок. Заменил макросы на нормальные функции, подправил их как требовалось, тесты соответствующие написал — все замечательно работает.

Через пару дней прибегает представитель соседнего отдела и говорит, что у них все падает и плющится, и все по вине моих функций. Иду разбираться. Гляжу — компоновщик не может найти функции, которые вызывались в старых макросах. Ну делов-то, говорю, пересоберите проект, я там макросы на функции заменил. Какой тут крик начался… У них же софтина уже прошла испытания, и они ее в архив сдали, вместе с контрольной суммой. Пересобрать ее — значит заново кучу бумажной волокиты произвести. Ой…

Что поделать, пересобрали. А я с тех пор к макросам отношусь с большой осторожностью.

УжасноПлохоНормальноХорошоОтлично (2 голосов, средний: 4,00 из 5)
Loading ... Loading ...

typedef

Ко мне в блог регулярно приходят люди по поисковым запросам вроде «typedef», «как работает typedef» или «что такое typedef пожалуйста помогите». Ничего удивительного — написанное аршинными буквами ключевое слово языка Си в заголовке может запутать не только поисковые машины. Посему не смею больше вводить в заблуждение своих читателей, публикую небольшой ликбез на тему.

typedef — это способ создания синонима типа. Ничего сакрального при этом не происходит, и программы не приобретают никаких особых свойств. Просто иногда это позволяет несколько упростить код, освободив его от таинственных определений вроде:

void (*signal(int sig, void (*func)(int)) )(int);

Приведенная декларация является потолком сложности среди встречающихся на практике определений. Авторы книг по Си могут сколько угодно приводить примеры «массива указателей на функции, принимающих указатели на массив структур и возвращающих константный указатель на функцию, возвращающую… бла-бла-бла». Такие извращения реально встречаются в одной программе из тысячи. Да и то, при виде такого нужно усомниться во вменяемости автора.

Короче говоря, typedef используется для сокращения сложных или просто длинных определений типов, или же для сокрытия реально используемых типов, например, для облегчения портирования.

Синтаксис typedef очень прост: мы пишем обычное объявление стековой переменной и добавляем перед этим определением ключевое слово typedef. При этом никакая переменная не создается, а имя «переменной» становится синонимом указанного типа. Например:

typedef const char* string; /* string - указатель на константный символ */
typedef void (*fptr)(int); /* fptr - указатель на функцию, возвращающую void и принимающую int */

Часто можно видеть, как с помощью typedef можно избавиться от необходимости писать struct при определении переменной типа структуры:

struct s_tag {
/* какие-то члены */
};
typedef struct s_tag mystruct;
/* ... */
mystruct s;

Можно создать синоним одновременно с определением структуры:

typedef struct s_tag {
/* какие-то члены */
} mystruct;

Можно даже создать синоним для безымянной структуры:

typedef struct {
/* какие-то члены */
} mystruct;

Начинающие программисты вместо typedef часто используют препроцессор:

#define mystruct struct s_tag

Хотя со структурами вроде бы никаких проблем не возникает, использование в общем случае препроцессора для создания псевдонима типа может привести к трудноуловимым ошибкам:

#define iptr int*
iptr ptr1, ptr2; /* какой тип у ptr2? */
УжасноПлохоНормальноХорошоОтлично (18 голосов, средний: 4,22 из 5)
Loading ... Loading ...

Минималистичный фреймворк модульных тестов для Си

Как-то на работе (а разрабатываем мы ни много ни мало — операционную систему) появилась необходимость писать модульные тесты к разрабатываемым функциям. В силу специфики разработки прикрутить какой-то внешний фреймворк не представлялось возможным. ОС все же вещь в себе, а без автоматизированного тестирования жизнь становится уж слишком непредсказуемой. В общем, пришлось написать свой юнит-тест-фреймворк.

Признаюсь сразу, идею слизал у MinUnit, но сам он мне показался неудобным. В результате получился один-единственный заголовочный файл:

#ifndef _UTEST_H_
#define _UTEST_H_
#include <stdio.h>
#define UT_ASSERT_EX(expr,fmt,...) \
    do { \
    if (!(expr)) { printf("ASSERT: file \"%s\", line %d, function \"%s\": ",\
            __FILE__, __LINE__, __FUNCTION__); \
            printf(fmt, __VA_ARGS__); \
            printf("\n"); \
            return 1; } \
    } while(0);
#define UT_ASSERT(expr,msg) UT_ASSERT_EX(expr, "%s", msg)
#define UT_TEST(test) \
    do { \
        printf("TEST: \"%s\" %s\n", #test, (_t_ ## test() ? "failed" : "OK")); \
    } while(0);
#define UT_TESTDEF(name, body) \
    static int _t_ ## name () { \
        do { \
            body \
        } while(0); \
        return 0; }
#endif /*_UTEST_H_*/

Использование, я полагаю, достаточно очевидно. Но на всякий случай пример:

#include "utest.h"
 
int func1(int arg)
{
    return arg*arg;
}
 
UT_TESTDEF(func1,
    int ret;
    ret = func1(5);
    UT_ASSERT(ret == 25, "blah-blah");
    ret = func1(0);
    UT_ASSERT_EX(ret == 0, "ret is %d, must be 0", ret);
);
 
int main()
{
    UT_TEST(func1);
    return 0;
}

Задачу свою такой подход вполне решает, так что невеликие размеры «фреймворка» пусть вас не смущают. Желающие могут навертеть вокруг этого свои рюшечки, а по мне и этого достаточно.

УжасноПлохоНормальноХорошоОтлично (2 голосов, средний: 5,00 из 5)
Loading ... Loading ...

День программиста и IOCCC

Хотя у меня пока, учитывая возраст блога, есть некоторое ощущение «разговора с пустотой», все же я не могу не поздравить всех немногочисленных моих посетителей с 0×100-ым днем в году. Это тот самый день когда нужно или не работать совсем, или сворачивать горы. У меня получилось второе: напланировал кучу дел, приехал на работу и через полтора часа уже все сделал. Остается только пожелать всем побольше таких ошибок в планировании. Или это все же сакральное влияние даты?

О серьезных вещах сегодня писать — кощунство, поэтому буду писать о полусерьезных.

PDP-7В далекие-далекие годы, когда еще только-только стали появляться компьютеры на полупроводниках, а в лесах можно было встретить динозавров, появился UNIX. Дорог он нам не только фактом своего существования, но главным образом сложившейся вокруг него культурой. Ну, знаете, там были такие волосатые очкарики, которые все свое время просиживали за терминалом (при наличии такового),  пили колу и разговаривали друг с другом на языке инопланетян. Эти ненормальные идентифицировали себя «хакерами», все время писали какой-то код и ни во что не ставили интеллектуальную собственность.  Кстати, все ли знают, что UNIX изначально создавался Томпсоном как средство запуска игры Space Travel на компьютере PDP-7?

Но нужно отдать должное этим нечесаным парням: в их развлечениях родилось много полезных штук, и, в частности, язык Си. Создали его изначально не для развлечений (хотя кто-то пытается это опровергать), а для тотального переписывания Юникса. Но хакеров ведь хлебом не корми — дай любую попавшую в руки сущность разобрать, понять как работает, перекроить и раздать друзьям. Поэтому, учитывая специфику C, нетрудно было догадаться, что мерилом профессионализма, наряду с длиной волос и диоптриями близорукости, станет способность написать самую необычную, изящную, умопомрачительную и непонятную простым смертным программу. Например, такую:

typedef struct n{int a:3,
b:29;struct n*c;}t;t*
f();r(){}m(u)t*u;{t*w,*z;
z=u-&gt;c,q(z),u-&gt;b=z-&gt;b*10,
w=u-&gt;c=f(),w-&gt;a=1,w-&gt;c=z-&gt;
c;}t*k;g(u)t*u;{t*z,*v,*p,
*x;z=u-&gt;c,q(z),u-&gt;b=z-&gt;b,v
=z-&gt;c,z-&gt;a=2,x=z-&gt;c=f(),x
-&gt;a=3,x-&gt;b=2,p=x-&gt;c=f(),p
-&gt;c=f(),p-&gt;c-&gt;a=1,p-&gt;c-&gt;c=
v;}int i;h(u)t*u;{t*z,*v,*
w;int c,e;z=u-&gt;c,v=z-&gt;c,q(
v),c=u-&gt;b,e=v-&gt;b,u-&gt;b=z-&gt;b
,z-&gt;a=3,z-&gt;b=c+1,e+9&gt;=c&amp;&amp;(
q(z),e=z-&gt;b,u-&gt;b+=e/c,w=f(
),w-&gt;b=e%c,w-&gt;c=z-&gt;c,u-&gt;c=
w);}int(*y[4])()={r,m,g,h};
char *sbrk();main(){t*e,*p,*o;
o=f(),o-&gt;c=o,o-&gt;b=1,e=f(),
e-&gt;a=2,p=e-&gt;c=f(),p-&gt;b=2,
p-&gt;c=o,q(e),e=e-&gt;c,(void)write
(1,"2.",2);for(;;e=e-&gt;c){q(e),
e-&gt;b=write(1,&amp;e-&gt;b["0123456789"],
1);}}t*f(){return i||(i=1000,
k=(t*)sbrk(i*sizeof(t))),k+--i;
}q(p)t*p;{(*y[p-&gt;a])(p);}

Эта программа, например, вычисляет и выводит на stdout значение числа e с неограниченной точностью (точнее, ограниченной размером стека или терпением пользователя). Понятно, что нанимателям таких программистов очень не нравился подобный стиль. История умалчивает, сколько хакеров было уволено, пока, наконец, в 1984 году не был организован IOCCC, или, по-нашенски, Международный Конкурс На Самую Запутанную Программу На C. Это было настоящей отдушиной для недосамовыраженных профессионалов, и работодатели снова могли спать спокойно.

Среди победителей IOCCC оказалось немало известных в компьютерном мире людей. Например, Дэвид Корн (создатель оболочки ksh) в 1987 году стал одним из победителей с такой программой:

main() { printf(&amp;unix["\021%six\012\0"],(unix)["have"]+"fun"-0x60);}

Основная интрига в том, что должно появиться на экране. Ничего сложно здесь нет, если помнить о том, что следующие две строки с точки зрения C идентичны:

a = b[1];
a = 1[b];

Плюс еще нужно знать, что unix — предопределенный символ, эквивалентный 1, если использовать gcc. В этом можно убедиться с помощью следующей команды:

$ touch foo.h; cpp -dM foo.h
...
#define unix 1
...

Если сложить два и два, получится, что программа выводит строку «unix».

Одна из моих любимых программ-чемпионов, написанная Марком Биггаром, выглядит так:

P;

Вот оно, настоящее Дао! Эта программа, в отличие от всех остальных победителей, умеет делать все. Ее нужно просто правильно скомпилировать. Например, можно заставить ее вывести «Hello, world»:

$ cc -DP="main() { printf(\"Hello, world\\n\"); }" -o beggar beggar.c
$ ./beggar
Hello, world

Нетрудно придумать, как научить программу делать и любые другие действия.

Кстати, на этом же соревновании засветился и Ларри Уолл, написавший некую линвистическую чуду-юду. Теперь понятно, откуда у Perl ноги растут. Оказывается, Уолл просто сделал язык, на котором непонятные программы писать гораздо проще, чем на C.

Пожалуй, на этом праздничный пост пора завершать. На досуге можете посмотреть исходники других победителей IOCCC, гарантированно получите массу удовольствия.

УжасноПлохоНормальноХорошоОтлично (4 голосов, средний: 4,50 из 5)
Loading ... Loading ...

Присваивание массивов

Каждый ребенок знает, что массивы и указатели в Си — это не одно и то же. Я обязательно посвящу этому волнующему вопросу один из будущих постов, но сейчас существенно то, что имя массива не может выступать в качестве lvalue. Попросту говоря — массиву нельзя ничего присвоить, поскольку его адрес определяется при компиляции; единственное исключение — первоначальная инициализация, вроде такой:

int a[5] = { 1, 2, 3, 4, 5 };

Если же мы попробуем провернуть такой же фокус в дальнейшем:

15
16
17
int a[3] = { 1, 2, 3 };
int b[3] = { 3, 2, 1 };
a = b;

получим по сусалам от компилятора:

$ cc -o0 -g -o test test.c
test.c: In function 'main':
test.c:17: error: incompatible types in assignment

Казалось бы, типы одинаковые: int[3], но нет, не дают присвоить. Такой вот семантический парадокс.
Но это еще не самое интересное. Самое интересное — массивы таки можно присвоить (читай — скопировать), безо всяких memcpy. Вот глядите:

struct s_tag {int arr[100];} aa, bb;
int i;
for (i = 0; i < 100; i++)
    aa.arr[i] = i;
bb = aa;

Все прекрасно компилируется и работает, и именно так, как предполагалось: один массив копируется в другой, цикл копирования генерируется компилятором.

Стоит добавить к этому, что при передаче массива в качестве аргумента функции копирование не происходит — передается только адрес. Зато если массив обернуть в структуру и передать ее по значению функции — копируется.

Мораль: массивы — это не совсем указатели и не совсем массивы; во избежание недоразумений необходимо учить матчасть.

УжасноПлохоНормальноХорошоОтлично (6 голосов, средний: 4,50 из 5)
Loading ... Loading ...