Доброго времени суток, господа и дамы! Иногда у некоторых людей возникает желание заняться откровенным непотребством в программировании — то, что не несет практической пользы напрямую, но помогает развлечься. И я — не исключение. В этой статье я хочу рассказать вам о лайфхаках, трюках (магических и не очень), алгоритмах на языке C!
Идея написать эту статью зародилась из моего поста, после него я начал серию статей, которая раскрывала много интересных моментов — от математических алгоритмов и оптимизации до ГПСЧ.
Увидев, что многим понравилась, я решил продолжить данную серию статей. Части независимы друг от друга, в каждой статье я прикладываю бенчмарки и графики.
В этой статье будет еще порция свежих хаков, фанов, трюков, еще больше магии и скорости!
Всех, кто заинтересовался — прошу под кат.
❯ Содержание
Вы можете читать статьи в любом порядке, они независимы друг от друга:
«Математика, биты, магия и немного ненормального программирования на C»
«Фокусы, хаки, магия и прочее ненормальное программирование на C»
Наступила юбилейная, пятая часть серии статей.
❯ Алгоритм Флетчера‑32
Данный алгоритм позволяет вычислять 32‑битную контрольную сумму. Разработан Джоном Флетчером из Лаборатории Лоуренса Ливермора в конце 1970‑х годов.
Алгоритм включает разделение слова двоичных данных на короткие «блоки» битов и вычисление модульной суммы этих блоков. Процесс:
Инициализировать две суммы —
sum1иsum2— в ноль.-
Для каждого 16‑битного слова (2 байта) во входных данных:
Добавить значение слова к
sum1, взяв результат по модулю 65 535.Добавить новое значение
sum1ко второй сумме, взяв результат по модулю 65 535.
Окончательная 32‑битная контрольная сумма формируется путем объединения
sum2(наиболее значащих 16 бит) иsum1(наименее значащих 16 бит). Результат:Result = (sum2 << 16) | sum1.
Среди особенностей можно выделить, что он чувствителен к порядку блоков, а также алгоритм не является криптографически безопасным — он предназначен для проверки целостности данных, а не для проверки подлинности.
Давайте перейдем к самой сишной реализации:
uint32_t fletcher32(const uint16_t* data, size_t len) {
uint32_t sum1 = 0xffff, sum2 = 0xffff;
while (len) {
size_t tlen = len > 360 ? 360 : len;
len -= tlen;
do {
sum1 += *data++;
sum2 += sum1;
} while (--tlen);
sum1 = (sum1 & 0xffff) + (sum1 >> 16);
sum2 = (sum2 & 0xffff) + (sum2 >> 16);
}
sum1 = (sum1 & 0xffff) + (sum1 >> 16);
sum2 = (sum2 & 0xffff) + (sum2 >> 16);
return sum2 << 16 | sum1;
}
Алгоритм работает с массивом 16‑битных слов. В основе лежат две аккумулирующие суммы, инициализированные значением 0xffff. На каждом шаге основного цикла к sum1 прибавляется очередное 16‑битное значение из входного массива, а к sum2 прибавляется текущее значение sum1. Это создает зависимость между всеми элементами данных.
Ключевая особенность реализации — обработка данных блоками по 360 элементов. Это предотвращает возможное переполнение при больших объемах данных. После обработки каждого блока выполняется операция модульной редукции: младшие 16 бит складываются со старшими 16 битами, что равно вычислению остатка от деления на 2¹⁶.
После обработки всех данных выполняется финальная редукция сумм, и результат формируется как объединение sum2 (в старших 16 битах) и sum1 (в младших 16 битах). Такой подход обеспечивает хорошее обнаружение различных типов ошибок, включая перестановки соседних слов.
Ниже я предоставил функцию для того, чтобы получать контрольную сумму строки:
uint32_t fletcher32_string(const char* str) {
size_t len = strlen(str);
size_t padded_len = (len + 1) / 2;
uint16_t* data = (uint16_t*)str;
return fletcher32(data, padded_len);
}
Данный алгоритм в итоге корректно работает и может показывать следующий результат:
Строка: "Fletcher32 checksum test"
Результат: 0x646d915c
❯ ГПСЧ TinyMT32
TinyMT — генератор псевдослучайных чисел, легкий вариант Mersenne Twister (MT). Представлен в 2011 году Муцуо Сайто и Макото Мацумото. Основное преимущество TinyMT32 — маленький размер внутреннего состояния. И так же, как большинство ГПСЧ, он не подходит для криптографии.
Структура tinymt32_t содержит внутреннее состояние генератора в виде массива из четырех 32‑битных целых чисел.
Кроме того, этот ГПСЧ требует сначала инициализации себя, функция tinymt32_init устанавливает начальное состояние на основе переданного seed. Первый элемент состояния инициализируется значением сида, остальные — константами. Затем выполняется 8‑раундная инициализация с обновлением состояния. Операции сдвига и XOR обеспечивают хорошее распространение сида по всему состоянию.
А вот сама функция генерации выполняет одно преобразование состояния и возвращает псевдослучайное число. Сначала вычисляется промежуточное значение x путем комбинации битов из трех элементов состояния с использованием маски 0x7FFFFFFF и операции XOR. Затем это значение сдвигается влево и снова применяется XOR. Далее выполняется циклический сдвиг элементов состояния, где первые три элемента сдвигаются, а последний вычисляется на основе x и предыдущего состояния. Возвращаемым значением становится новый вычисленный элемент state[3].
typedef struct {
uint32_t state[4];
} tinymt32_t;
void tinymt32_init(tinymt32_t *tmt, uint32_t seed) {
tmt->state[0] = seed;
tmt->state[1] = 0x8f7011ee;
tmt->state[2] = 0xfc78ff1f;
tmt->state[3] = 0x3793fdff;
for (int i = 1; i < 8; i++) {
tmt->state[i & 3] ^= i + 1812433253 *
(tmt->state[(i-1) & 3] ^ (tmt->state[(i-1) & 3] >> 30));
}
}
uint32_t tinymt32_generate(tinymt32_t *tmt) {
uint32_t x = (tmt->state[0] & 0x7fffffff) ^ tmt->state[1] ^ tmt->state[2];
x ^= x << 1;
tmt->state[0] = tmt->state[1];
tmt->state[1] = tmt->state[2];
tmt->state[2] = tmt->state[3] ^ (x >> 1);
tmt->state[3] = x;
return tmt->state[3];
}
Об этом алгоритме можно прочитать на странице RFC 8682.
❯ Хеш‑функция FNV‑1a
Давайте перейдем к теме хеш‑функций. FNV‑1a (Fowler–Noll–Vo) — одна из них. Она не подходит для криптографии, но позволяет быстро вычислять значения и подходит для хеш‑таблиц.
Алгоритм начинается с инициализации хеш‑значения начальным числом 2166136261. Затем для каждого байта входных данных выполняется две операции: операция XOR текущего хеша со значением байта, за которой следует умножение на 16777619.
uint32_t fnv1a_hash(const void *data, size_t len) {
const uint8_t *bytes = (const uint8_t*)data;
uint32_t hash = 2166136261u;
for (size_t i = 0; i < len; i++) {
hash ^= bytes[i];
hash *= 16777619u;
}
return hash;
}
❯ Bit reversal (переворот битов)
Данная функция выполняет зеркальную перестановку битов в 32‑битном числе (бит 0 меняется местами с битом 31 и т. д.).
В начале алгоритм меняет местами соседние биты: сдвиг вправо четных битов, влево нечетных, а после — объединение. На втором шаге меняются местами соседние пары битов. Аналогичный сдвиг битов и объединение. Третьим этапом меняет местами соседние четверки битов, после идет смена мест между соседними байтами, и последний пятый шаг меняет местами два 16‑битных полуслова. Здесь используется простой сдвиг: старшие 16 бит сдвигаются вправо, младшие — влево, и результаты объединяются.
uint32_t reverse_bits(uint32_t x) {
x = ((x >> 1) & 0x55555555) | ((x & 0x55555555) << 1);
x = ((x >> 2) & 0x33333333) | ((x & 0x33333333) << 2);
x = ((x >> 4) & 0x0F0F0F0F) | ((x & 0x0F0F0F0F) << 4);
x = ((x >> 8) & 0x00FF00FF) | ((x & 0x00FF00FF) << 8);
x = (x >> 16) | (x << 16);
return x;
}
В результате после последовательных перестановок биты полностью меняются порядком на обратный. Алгоритм эффективен и выполняется за O(log n) операций, где n — количество битов.
❯ MicroLCG — ГПСЧ с 8‑битным состоянием
Данный код реализует линейный конгруэнтный генератор (ЛКГ) для генерации псевдослучайных чисел в диапазоне 0–255.
Линейный конгруэнтный генератор (ЛКГ) — это простейший алгоритм генерации последовательности псевдослучайных чисел, основанный на рекуррентной формуле:
Xₙ₊₁ = (a * Xₙ + c) mod m
Работает он следующим образом: функция принимает указатель на 8‑битное число state, которое является текущим состоянием генератора. Внутри функция вычисляет новое значение состояния по формуле: новое состояние = 29 * (текущее состояние) + 217. Операции выполняются с использованием арифметики по модулю 256, поскольку тип uint8_t автоматически обрезает результат до младших 8 бит (это эквивалентно взятию по модулю 256).
uint8_t micro_rand(uint8_t *state) {
*state = 29 * (*state) + 217;
return *state;
}
❯ ГПСЧ «Mulberry32» (сверхкомпактный)
Генератор обновляет 32‑битное состояние *state прибавлением константы 0x6D2B79F5. Рабочая копия z проходит два раунда перемешивания.
Каждый раунд использует форму z = (z ^ (z >> N)) * (z | 1). Умножение на (z | 1) (всегда нечётное) — биективная операция по модулю 2³², она не теряет энтропию. XOR со сдвигом z ^ (z >> N) вносит нелинейность, распространяя изменения между битами.
После второго раунда применяется выходная функция z ^ (z >> 14). Она улучшает статистику младших битов, выравнивая их случайность со старшими.
uint32_t mulberry32(uint32_t *state) {
uint32_t z = (*state += 0x6D2B79F5);
z = (z ^ (z >> 15)) * (z | 1);
z ^= z + (z ^ (z >> 7)) * (z | 1);
return z ^ (z >> 14);
}
Вся логика — это цепь необратимых (сложение) и биективных (умножение на нечётное, XOR со сдвигом) операций. Это создаёт хаотичное преобразование одного слова состояния, достаточное для многих практических задач.
❯ ГПСЧ «RanQ1» (Quick 64‑bit)
Всего одно 64‑битное состояние. Три операции XOR‑shift и одно умножение. Самый быстрый ГПСЧ на диком западе.
uint64_t ranq1_state;
uint64_t ranq1() {
ranq1_state ^= ranq1_state >> 21;
ranq1_state ^= ranq1_state << 35;
ranq1_state ^= ranq1_state >> 4;
return ranq1_state * 2685821657736338717ULL;
}
Этот генератор работает с одним‑единственным числом — 64‑битным состоянием. Каждый раз, когда нужно новое случайное число, он его быстро взбалтывает и преобразует. Сначала он трижды перемешивает своё состояние с помощью операций, похожих на сдвиг и наложение битов (XOR‑shift). Это как быстро перетасовать колоду: сдвинуть часть битов вправо, наложить на себя, потом сдвинуть влево, снова наложить, и ещё раз немного сдвинуть. Эти три быстрые манипуляции ломают простые закономерности в битах.
Вся хитрость в том, что каждая операция (сдвиг, XOR, умножение) — невероятно быстра для процессора.
❯ Потоковый шифр RC4
Давайте рассмотрим потоковый шифр RC4. RC4 (от англ. Rivest cipher 4 или Ron’s code) — потоковый шифр, разработанный в 1987 году Рональдом Ривестом, сотрудником компании RSA Security. Он состоит из двух основных частей: инициализации и генерации псевдослучайного байта.
Потоковый шифр — это симметричный шифр, в котором каждый символ открытого текста преобразуется в символ шифрованного текста в зависимости не только от используемого ключа, но и от его расположения в потоке открытого текста.
typedef struct {
uint8_t S[256];
int i, j;
} RC4_ctx;
Структура RC4_ctx хранит внутреннее состояние шифра: массив S из 256 байт для перестановки и два индекса i и j для работы алгоритма. Это состояние необходимо сохранять между генерациями байтов. Предоставлена она выше.
void rc4_init(RC4_ctx *ctx, const uint8_t *key, int key_len) {
for (int i = 0; i < 256; i++) ctx->S[i] = i;
int j = 0;
for (int i = 0; i < 256; i++) {
j = (j + ctx->S[i] + key[i % key_len]) & 0xFF;
uint8_t tmp = ctx->S[i];
ctx->S[i] = ctx->S[j];
ctx->S[j] = tmp;
}
ctx->i = ctx->j = 0;
}
Функция выше инициализирует состояние шифра. Она сначала заполняет массив S последовательными числами от 0 до 255. Затем она выполняет ключевое расписание, перемешивая этот массив с помощью ключа. Для каждого байта массива вычисляется индекс j на основе текущего j, значения S[i] и байта ключа, после чего элементы S[i] и S[j] меняются местами. Это создаёт уникальную начальную перестановку, зависящую от ключа. В конце функция сбрасывает индексы i и j в ноль, переводя шифр в начальное состояние для генерации потока.
uint8_t rc4_byte(RC4_ctx *ctx) {
ctx->i = (ctx->i + 1) & 0xFF;
ctx->j = (ctx->j + ctx->S[ctx->i]) & 0xFF;
uint8_t tmp = ctx->S[ctx->i];
ctx->S[ctx->i] = ctx->S[ctx->j];
ctx->S[ctx->j] = tmp;
return ctx->S[(ctx->S[ctx->i] + ctx->S[ctx->j]) & 0xFF];
}
Функция rc4_byte генерирует один псевдослучайный байт ключевого потока. Она сначала обновляет индексы i и j: увеличивает i на 1, а j — на значение S[i]. Затем снова меняет местами элементы S[i] и S[j]. На выходе функция возвращает байт из массива S, индекс которого вычисляется как сумма значений только что обменянных S[i] и S[j]. Этот байт и является частью ключевого потока, который можно использовать для шифрования путём операции XOR с открытым текстом.
❯ XOR Linked List
Давайте перейдем к классическим структурам данных. Обычный связный список, но хранящийся в одном указателе через XOR. Ключевая идея: если известен один из соседних узлов, можно получить другой, выполнив XOR с полем link.
typedef struct xor_node {
int value;
uintptr_t link;
} xor_node;
Узел хранит значение и поле link, которое содержит результат побитового XOR адресов предыдущего и следующего узлов. Используется uintptr_t для безопасного преобразования указателя в число.
void xor_list_add(xor_node *prev, xor_node *node, xor_node *next) {
node->link = (uintptr_t)prev ^ (uintptr_t)next;
prev->link ^= (uintptr_t)next ^ (uintptr_t)node;
next->link ^= (uintptr_t)prev ^ (uintptr_t)node;
}
xor_node* xor_list_next(xor_node *prev, xor_node *current) {
return (xor_node*)(current->link ^ (uintptr_t)prev);
}
Функция xor_list_add получает три готовых узла: предыдущий, вставляемый и следующий. Связь нового узла формируется как XOR адресов его соседей. Затем корректируются связи существующих соседей. У предыдущего узла в его поле связи адрес старого следующего заменяется адресом нового узла через операцию XOR. И тот же принцип применяется для следующей ноды.
А для получения следующей ноды (xor_list_next) нужны указатели на текущий и предыдущий узел. Если выполнить операцию XOR этого поля с адресом известного предыдущего узла, результатом будет адрес следующего узла, так как операция обращает сама себя.
❯ Бонус: меняем мантиссу числа на инвертированную
Эта функция меняет мантиссу числа на инвертированную, оставляя экспоненту неизменной. Создает «зеркальные» числа с плавающей точкой. Чисто для фана.
float magic_float_trick(float x) {
union { float f; uint32_t i; } u = {x};
u.i = (u.i & 0x7F800000) | (~u.i & 0x007FFFFF);
return u.f;
}
Данный алгоритм берет float, копирует его экспоненту как есть, а дробную часть (мантиссу) меняет на ее битовую инверсию. Знак всегда становится плюсом. Получается число той же величины (порядка), но со странной дробной частью.
❯ Бенчмарки
Не стану изменять традиции, я также написал бенчмарки наших функций:
Math Algorithms Performance (1000000 iterations):
--------------------------------------------
fast_pow: 3.28 ms ( 0.003 us/call)
fastest_pow: 2.32 ms ( 0.002 us/call)
fast_mod: 2.36 ms ( 0.002 us/call)
is_power_of_two: 2.78 ms ( 0.003 us/call)
jenkins_hash: 60.47 ms ( 0.060 us/call)
jenkins_mix+final: 60.55 ms ( 0.061 us/call)
xor_swap: 4.51 ms ( 0.005 us/call)
div3: 2.79 ms ( 0.003 us/call)
isqrt: 12.40 ms ( 0.012 us/call)
to_gray: 2.79 ms ( 0.003 us/call)
from_gray: 3.25 ms ( 0.003 us/call)
counting_sort_256: 15.57 ms ( 1.557 us/call)
next_power_of_two: 2.81 ms ( 0.003 us/call)
count_trailing_zeros: 3.56 ms ( 0.004 us/call)
count_trailing_zeros_kernighan: 3.63 ms ( 0.004 us/call)
fisher_yates_shuffle: 4.27 ms ( 0.427 us/call)
Q_rsqrt: 3.25 ms ( 0.003 us/call)
reverse_bits: 2.79 ms ( 0.003 us/call)
--------------------------------------------
PRNG Performance (10,000,000 iterations):
-----------------------------------------
xorshift64: 30.94 ms (323.20M numbers/s)
lehmer64: 44.46 ms (224.92M numbers/s)
xoshiro256pp: 32.52 ms (307.46M numbers/s)
pcg32: 23.23 ms (430.55M numbers/s)
wyrand: 36.15 ms (276.61M numbers/s)
msws32: 35.61 ms (280.85M numbers/s)
romu_duo: 27.91 ms (358.27M numbers/s)
sfc32: 89.55 ms (111.67M numbers/s)
sha1_prng: 526.40 ms ( 19.00M numbers/s)
tinymt32: 98.71 ms (101.31M numbers/s)
mulberry32: 27.86 ms (358.88M numbers/s)
ranq1: 32.54 ms (307.35M numbers/s)
rc4_byte: 48.96 ms (204.23M numbers/s)
-----------------------------------------
Hash Algorithms Performance (1000000 iterations):
--------------------------------------------
jenkins_hash: 124.68 ms ( 0.125 us/call)
fnv1a_hash: 45.28 ms ( 0.045 us/call)
fletcher32_string: 18.81 ms ( 0.019 us/call)
--------------------------------------------
И также я реализовал графики, чтобы вы визуально могли изучить разницу:






❯ Заключение
Спасибо за прочтение статьи! Я надеюсь, вы узнали что‑то новенькое, или, может, какой‑нибудь трюк натолкнул вас на другой интересный алгоритм. Если нашли нюанс в самой статье — пишите в комментарии.
Если вам понравился изложенный материал, могу предложить вам подписаться на мой блог в телеграме. Если, конечно, вам статья понравилась и вы хотите видеть чуть больше.
Примеры работы кода вы можете увидеть в моем репозитории The Art Of Fun C.
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩
Комментарии (10)

Biga
23.12.2025 09:02uint16_t* data = (uint16_t*)str;Ох, это опасно! Лучше не делайте так.
Рискуете упасть на arm из-за выравнивания.

sergio_nsk
23.12.2025 09:02Зачем удалять и репостить статью "Ненормальные Непотребства, Трюки, Хаки И Алгоритмы На C" неделей ранее?

DrArgentum Автор
23.12.2025 09:02План публикации перепутан, нумерация, а так как в статье есть ссылки на прошлые лучше не ломать нумерацию.

mpa4b
23.12.2025 09:02Одному мне кажется, что к любому предлагаемому PRNG должны прилагаться результаты его испытаний хотя бы на dieharder ?

alvoskov
23.12.2025 09:02Dieharder не очень чувствителен, комбинация из TestU01 и PractRand хотя бы на 2 ТиБ куда лучше. И то иногда они порой пропускают игрушечные генераторы. Ещё важной характеристикой ГПСЧ я считаю то, во сколько раз он быстрее ChaCha8 и AES128 на данной платформе.

DrArgentum Автор
23.12.2025 09:02Статья посвящена общим трюкам, а для ГПСЧ я планировал отдельную статью. Но спасибо за фидбек!
alvoskov
О генераторах из статьи: RanQ1 ещё встречается под другим названием - xorshift64*, и с немного другими константами. У всех его версий есть статистические дефекты в младших битах, хотя в зависимости от их констант выраженность может различаться. TinyMT32 и даже RC4 тоже PractRand не пройдут. Кстати, на RC4 похожи ISAAC и ISAAC64, и они раз в 5 быстрее его и ощутимо качественнее.
Среди ГПСЧ есть ещё очень интересная разновидность линейного конгруэнтного генератора - MWC, Multiply With Carry. Они с помощью некоторых арифметических трюков эмулируют деление на большой простой делитель. Генераторы из них получаются быстрые и довольно приличные. Вот хорошие примеры:
https://cas.ee.ic.ac.uk/people/dt10/research/rngs-gpu-mwc64x.html
https://prng.di.unimi.it/MWC128.c (в нем предлагаемый авторами скремблер лучше включить)
DrArgentum Автор
Про xorshift я рассказывал в прошлых частях статьи, но я планирую выпустить статью про устройство ГПСЧ и туда включить старые и новые генераторы.
Спасибо за фидбек!
alvoskov
Здорово, было бы интересно прочитать, особенно если получится про "ненормальное программирование" и трюки. Правда, у меня после плотной работы с ГПСЧ возникло весьма странное их понимание: я стал считать трюкачеством любые некриптографические генераторы.