Архив автора

Кэширование и буферизация

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

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

Примеры использования кэширования:

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

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

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

  • временная неготовность устройства; при прожиге болванок наверняка все видели волшебный Progress Bar «Уровень буфера», это как раз то самое;
  • невозможность принять поток данных с требуемой скоростью; например, для этих целей существует исходящая очередь сетевых пакетов — приложения могут клепать данные быстрее, чем сетевая карта успеет их передавать;
  • гранулярность обмена; в этом случае буферизация служит средством группировки данных; яркий пример — жесткий диск, обмен с которым возможен только блоками размером с сектор (512 байт обычно), а лучше даже целыми дорожками (много секторов); это также пример того, как одна и та же структура данных может использоваться и для кэширования, и для буферизации (см. выше пример про кэширование жесткого диска).

Буферизация бывает прозрачной и непрозрачной. Прозрачная — когда обменивающиеся данными стороны в курсе, что буфер существует (clipboard, message queue). Непрозрачная буферизация — когда буферизация незаметна (сокеты).

Резюме. Основная задача кэширования — ускорить, а лучше вообще устранить чтение из медленной памяти. Основная задача буферизации — повысить эффективность записи.

Контрольный вопрос. Что такое буферный кэш?

УжасноПлохоНормальноХорошоОтлично (6 голосов, средний: 4,67 из 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 ...

Смерть башоргам!

Хотя всякие программерские хитрости и тонкости — безусловно штука полезная, но в вакууме, как известно, работают только сферические программисты (те самые, что пишут программы длиной в один байт). Реальные же люди вынуждены терпеть существование вокруг них всего остального мира в виде назойливых людей, телефонных звонков, развлекательных сайтов и хорошей погоды как раз тогда, когда на работе завал. Я даже не возьмусь сказать, что больше определяет производительность программиста: профессиональный уровень или способность абстрагироваться от внешних раздражителей.

Долгое время я страдал от такой вредной привычки. Когда в работе наступал ступор, например, окончание небольшого этапа или просто необходимость крепко подумать, я машинально открывал почту, потом RSS-ленту, потом новости, потом еще какой-то сайт, потом находил какую-то интересную статью… А когда возвращался к работе, оказывалась, что можно, в общем-то, уже не начинать, потому что скоро обед / конец рабочего дня / whatever. Короче говоря, продуктивность моя сильно от этого дела страдала.

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

  1. подзадпинательную — мотивация на выполнение нужных действий и невыполнение ненужных;
  2. организационную — уже при наличии мотивации повысить эффективность выполняемых действий.

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

  • Необходимо вести список дел, которые необходимо выполнить. Позволить себе обходиться без такого списка могут только те, кто долгое время им пользовался и поняли, что способны быть эффективными и без него. Я составляю список дел на каждый день, плюс отдельные списки отдаленных дел и дел вообще без временной привязки. Каждый вечер и каждое утро я просматриваю все эти списки и формирую план действий на предстоящий день. В списке делаю  пометки, показывающие особый статус дела (срочность, контекст и т.п.).
  • Список дел должен быть в бумажном блокноте, а блокнот — всегда с собой. К этому я пришел спустя годы пользования всевозможными программами. Главное преимущество блокнота — постоянная видимость. Как только я прихожу на работу, домой или куда-либо еще, сразу выкладываю его на стол, и все задачи у меня перед глазами весь день. Даже КПК не обладает таким свойством. Кроме того, бумага обеспечивает исключительную гибкость визуального представления.
  • Сюда же, в блокнот, я записываю все идеи, которые пришли в голову за день. Потом мысли, достойные сохранения, в обработанном виде переносятся в другой блокнот или, например, в блог.
  • Каждая задача из списка запланированных на сегодня не должна занимать более часа (если быть точнее, то должна быть возможность выполнить задачу за один заход, без перерывов). Если задача большая, то я разделяю ее на несколько более мелких. Нет ничего хуже, чем постепенно выполняемое в течение нескольких дней дело, постоянно мозоляющее глаза своей незавершенностью.
  • Не стоит расстраиваться, если какие-то дела, запланированные на день, остались невыполненными. Это нормально. Их можно просто перенести на следующий день. Если же одна и та же задача кочует по блокноту в течение недели или даже дольше, нужно задуматься, действительно ли эту задачу следует выполнять.
  • Все действия, требующие менее 5 минут, должны выполняться немедленно.
  • Каждое выполняемое действие должно иметь цель. Это не значит, конечно, что нужно над каждым чихом размышлять — «А соответствует ли этот чих моим Принципам? А приближает ли он меня к моей Цели?». До абсурда доходить не нужно, но стараться все же себя контролировать. Например, открывая браузер, надо четко представлять, что именно мы хотим найти или узнать. И если сумеем ответить на этот вопрос, определить еще, действительно ли нам нужно то, что мы хотим узнать. Неочевидное следствие из этого правила: дела из блокнота имеют преимущество над делами, там не перечисленными.
  • Перед совершением любого действия спросить себя, оправдывает ли действие затрачиваемое на него время? Можно ли это време потратить с большей пользой?
  • Не планировать на день больше, чем реально можно успеть сделать.

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

Что касается башорга (в нарицательном смысле), то я избавился от привычки посещения не относящихся к делу сайтов очень просто: каждый день дописывал себе в todo-list задачу «Не ходить на развлекательные сайты». Через неделю я избавился от этой привычки полностью.

УжасноПлохоНормальноХорошоОтлично (11 голосов, средний: 4,91 из 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 ...

Выборочная инициализация массивов и структур

При всей своей примитивности язык C иногда преподносит приятные сюрпризы. Например, в совершенный восторг меня приводит способ инициализации структур и массивов под названием designated initializers (я предпочитаю переводить это как «выборочная инициализация»). Суть в том, что при инициализации переменной агрегатного типа можно явно указать, какие части структуры данных мы инициализируем, а какие — предоставляем обнулить компилятору. Звучит немного заумно, поэтому сразу приведу пару простых примеров.

Выборочная инициализация массива (работает в C, но не в C++!):

int a[] = {
    [2] = 2,
    [7] = 7
};

Выборочная инициализация структуры:

struct {
    int a;
    int b;
    int c;
} s = { .a = 1, .c = 2 };

Нетрудно догадаться, что здесь происходит. В первом случае мы получаем массив из 8 элементов, два из которых инициализированы, а остальным присваивается 0. Со структурой еще проще: поля a и c получают значения, а поле b обнуляется. Рассмотрим примеры посложнее.

struct {
    int a;
    struct {
        char c;
        int  d;
    } b;
    int c;
} s = {
    .a = 2,
    .b.d = 3
};
static short grid[3] [4] = { [0][0]=8, [0][1]=6,
                             [0][2]=4, [0][3]=1,
                             [2][0]=9, [2][1]=3,
                             [2][2]=1, [2][3]=1 };
int a[] = {2, 4, [8]=9, 10}

Если первые два случая еще как-то интуитивно понятны, то последний поначалу заставляет задуматься. Здесь действует простое правило, такое же, как и при объявлении enum. По умолчанию индексы в инициализаторе начинаются с 0 и с каждым новым значением увеличиваются на единицу. Но если указать индекс явно, он становится новым значением по умолчанию. Приведенный выше пример раскрывается в следующее:

int a[] = {[0]=2, [1]=4, [8]=9, [9]=10}

Таким образом, получим массив из 10 элементов.

К сожалению, из всякого правила есть исключения. C настолько гибок, что позволяет задурить самого себя. Дело в том, что разрешается инициализировать элементы массива в произвольном порядке. Как вы думаете, что произойдет при такой инициализации?

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

Могу сказать сразу, что синтаксически конструкция безупречна.

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

#define SIGNAME(s) [s] = #s
const char * const sigNames[] = {
    SIGNAME(SIGHUP),
    SIGNAME(SIGINT),
    SIGNAME(SIGQUIT),
    /* ... */
};
#undef SIGNAME

Теперь, если численные значения идентификаторов сигналов изменятся, нам ничего не придется менять, массив сам подстроится. Это, конечно, достаточно примитивный случай, но вполне демонстрирующий идею — как можно соблюсти принцип DRY и сделать код более аккуратным.

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