Доброго времени суток, господа и дамы! Иногда у некоторых людей возникает желание заняться откровенным непотребством в программировании — то, что не несет практической пользы напрямую, но помогает развлечься. И я — не исключение. В этой статье я хочу рассказать вам о лайфхаках, трюках (магических и не очень) и алгоритмах на языке C!
Идея написать эту статью зародилась из моего поста, после него я начал серию статей, которая раскрывала много интересных моментов — от математических алгоритмов и оптимизации до ГПСЧ.
Если вы видите эту статью, значит еще не все тайны C раскрыты. В этой статье будет еще больше свежих хаков, фанов, трюков, еще больше магии и скорости! А также нетипичных алгоритмов и структур данных, что позволит вам почерпнуть и полезную информацию тоже!
Добро пожаловать в новую часть. Прошу под кат — там будет жарко, быстро и очень, очень интересно.
❯ Содержание
Вы можете читать статьи в любом порядке, они независимы друг от друга:
«Математика, биты, магия и немного ненормального программирования на C»
«Фокусы, хаки, магия и прочее ненормальное программирование на C»
«Непотребные алгоритмы, ненормальные трюки и всевозможные хаки на C»
❯ Алгоритм bitboard для шахмат/игр
Использование 64-битных чисел для игрового поля — классика оптимизации.
#include <stdio.h> #include <stdint.h> typedef uint64_t Bitboard; Bitboard knights_attacks(Bitboard knights) { Bitboard l1 = (knights >> 1) & 0x7F7F7F7F7F7F7F7F; Bitboard l2 = (knights >> 2) & 0x3F3F3F3F3F3F3F3F; Bitboard r1 = (knights << 1) & 0xFEFEFEFEFEFEFEFE; Bitboard r2 = (knights << 2) & 0xFCFCFCFCFCFCFCFC; return (l1 | r1) << 16 | (l1 | r1) >> 16 | (l2 | r2) << 8 | (l2 | r2) >> 8; } void print_bitboard(Bitboard bb) { for (int row = 7; row >= 0; row--) { for (int col = 0; col < 8; col++) {3 int square = row * 8 + col; printf("%c ", (bb >> square) & 1 ? '1' : '.'); } printf("\n"); } printf("\n"); } int main() { Bitboard knight = 1ULL << 36; /* конь на e4*/ printf("позиция коня:\n"); print_bitboard(knight); printf("возможные ходы коня:\n"); Bitboard attacks = knights_attacks(knight); print_bitboard(attacks); return 0; }
Фанфакт: в английском как шахматная фигура конь это не horse, а knight. Ладья — Rook (грач/утес/башня). Слон — Bishop (епископ).
Функция knights_attacks вычисляет все возможные ходы коня за несколько битовых операций. Принцип такой: конь ходит буквой «Г» — на две клетки в одном направлении и одну в перпендикулярном.
Алгоритм делает сдвиги в четыре стороны. Сначала сдвигает позиции коней влево и вправо на 1 и 2 клетки (l1, r1, l2, r2). Маски 0x7F... и 0xFE... нужны чтобы не было «перетекания» битов через края доски — они отсекают боковые столбцы.
Затем комбинируем эти сдвиги. Объединение l1 и r1 дает горизонтальные смещения на одну клетку. Сдвиг этой комбинации на 16 рядов вверх и вниз дает вертикальное смещение на две клетки — получаем ходы типа «две вверх/вниз, одна вбок». Аналогично l2 и r2 дают смещение на две клетки по горизонтали. Сдвиг на 8 рядов дает смещение на одну клетку по вертикали — это ходы «одна вверх/вниз, две вбок».
Битовое ИЛИ всех четырех вариантов дает полную маску атак.
Главная прелесть — всего 10 битовых операций для любого количества коней на доске. Вместо перебора 64 клеток или циклов по фигурам — мгновенный расчет. Именно так работают современные шахматные движки.
При запуске мы получаем такой красивый вывод:
позиция коня: . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . возможные ходы коня: . . . . . . . . . . . 1 . 1 . . . . 1 . . . 1 . . . . . . . . . . . 1 . . . 1 . . . . 1 . 1 . . . . . . . . . . . . . . . . . .
❯ Неточное сравнение дробных чисел
Алгоритм решает проблему неточного сравнения чисел с плавающей точкой. Прямое сравнение через == часто даёт ложный результат из-за особенностей двоичного представления и накопления ошибок округления.
bool fuzzy_float_eq(float a, float b) { float abs_diff = fabsf(a - b); float max_abs = fmaxf(fabsf(a), fabsf(b)); return abs_diff <= max_abs * 1e-6f || abs_diff < 1e-12f; }
Работает функция вот так: сначала вычисляется абсолютная разница между числами. Затем определяется максимальное по модулю из двух чисел. Основная проверка использует два условия. Первое — относительная погрешность: абсолютная разница не превышает одной миллионной от максимального модуля. Это покрывает большинство случаев. Второе — абсолютная погрешность: разница меньше фиксированного малого значения, что нужно для корректной работы с числами, близкими к нулю, где относительная погрешность теряет смысл.
int main() { float num1 = 1.0000009901f; float num2 = 1.0000009902f; if (fuzzy_float_eq(num1, num2)) { printf("Числа считаются равными\n"); } else { printf("Числа различны\n"); } return 0; }
Но так как это нечеткое сравнение, данный код выдаст что числа num1 и num2 равные. По сути, суть алгоритма в том, что числа считаются равными, если они либо очень близки относительно их величины, либо отличаются на исчезающе малую величину.
❯ ГПСЧ GJF (Gonzalo Joseph’s Fast)
В этой серии статьи мы давно не затрагивали тему ГПСЧ!
#include <stdint.h> uint64_t gjf_state[2] = {0x123456789ABCDEF, 0xFEDCBA987654321}; uint64_t gjf64(void) { uint64_t s0 = gjf_state[0]; uint64_t s1 = gjf_state[1]; gjf_state[0] = s1; s0 ^= s0 << 23; gjf_state[1] = s0 ^ s1 ^ (s0 >> 17) ^ (s1 >> 26); return gjf_state[1] + s1; }
Это компактный и ⚡ blazingly fast ⚡ (и даже не надо переписывать на раст) генератор, который хранит состояние в двух 64-битных переменных. В основе — перестановка и смешивание битов через сдвиги и XOR.
Его работа основана на быстрых битовых операциях: сдвигах и XOR. В начале функции сохраняются текущие значения состояния в локальные переменные s0 и s1. Первое состояние обновляется простой перестановкой — оно становится равным старому второму состоянию.
Затем начинается перемешивание. Значение s0 подвергается операции XOR'а с собственной копией, сдвинутой влево на 23 бита. Этот этап вносит нелинейность и распространяет изменение битов. После этого вычисляется новое значение для второго элемента состояния. Оно представляет собой XOR четырёх компонентов: только что изменённого s0, старого s1, а также сдвинутых вправо на 17 бит версии s0 и на 26 бит версии s1. Такая комбинация сдвигов разной длины обеспечивает тщательное перемешивание битов со всего 128-битного пространства состояния.
Финальный шаг — генерация выходного числа. Функция возвращает сумму только что вычисленного нового второго состояния и старого значения s1. Это сложение улучшает статистические свойства вывода, делая распределение более равномерным. Весь цикл выполняется за несколько тактов процессора, что делает генератор одним из самых быстрых в своём классе. Его период огромен, а качества случайности достаточно для большинства симуляций, игр и задач, не требующих криптографической стойкости.
Давайте посмотрим на пример:
int main() { for (int i = 0; i < 5; i++) { printf("%016llx\n", gjf64()); } return 0; }
И код выдаст следующие шестнадцатеричные числа:
ccf7ce0251e21edb 232e19416e19e115 5d6f795c479bf4d2 dc8a1592370d6141 10e2fc8d1d6cc6f0
В этом коде значение стартового состояния предопределено, но вы можете реализовать генерацию сида.
❯ XXTEA — крошечный блочный шифр
Компактный и эффективный шифр для эмбединга, осдева или прочих ваших странных потребностей. XXTEA шифрует массив 32-битных слов налету, используя ключ из 4 слов. Алгоритм выполняет несколько раундов, на каждом смешивая данные с ключом через XOR, сдвиги и сложение.
#include <stdint.h> #define DELTA 0x9e3779b9 #define MX (((z>>5^y<<2) + (y>>3^z<<4)) ^ ((sum^y) + (key[(p&3)^e] ^ z))) void xxtea_encrypt(uint32_t *v, int n, uint32_t const key[4]) { uint32_t y, z, sum; unsigned p, rounds, e; if (n < 1) return; rounds = 6 + 52/n; sum = 0; z = v[n-1]; do { sum += DELTA; e = (sum >> 2) & 3; for (p=0; p<n-1; p++) { y = v[p+1]; z = v[p] += MX; } y = v[0]; z = v[n-1] += MX; } while (--rounds); }
Первое, что делает алгоритм — вычисляет число раундов. Формула 6 + 52/n гарантирует, что даже для больших массивов будет выполнено достаточное количество перемешиваний. Начинается цикл с обнулённой переменной sum.
В каждом раунде к sum прибавляется число DELTA. Затем определяется e — индекс для выбора слова ключа, основанный на текущей сумме.
Далее идёт сердце алгоритма — внутренний цикл по всем словам массива, кроме последнего. Для каждого слова вычисляется замысловатая функция MX. В ней через сдвиги, сложения и XOR перемешиваются соседние слова (y и z), текущая сумма и часть ключа. Результат этого микса прибавляется к текущему слову, что и является шифрованием.
После обработки основного массива отдельно шифруется последнее слово с первым, и раунд завершается. Этот процесс повторяется заданное количество раундов, постепенно и необратимо перемешивая все данные с ключом.
void xxtea_decrypt(uint32_t *v, int n, uint32_t const key[4]) { uint32_t y, z, sum; unsigned p, rounds, e; if (n < 1) return; rounds = 6 + 52/n; sum = rounds * DELTA; y = v[0]; do { e = (sum >> 2) & 3; for (p=n-1; p>0; p--) { z = v[p-1]; y = v[p] -= MX; } z = v[n-1]; y = v[0] -= MX; sum -= DELTA; } while (--rounds); }
Ну и также покажу функцию для дешифровки, ключевое отличие в обратном порядке операций и направлении. Если шифрование идёт «вперёд», то дешифрование идёт «назад», отменяя каждый шаг. Начальное значение sum не ноль, а rounds * DELTA — полная сумма, которая накопилась бы к концу шифрования. Важно понимать, что порядок обновления переменных y и z в дешифровании обратный по отношению к шифрованию, чтобы гарантировать, что при вычислении MX используются те же значения, что и при шифровании.
Ну и на примере демонстрация работы:
int main() { uint32_t data[] = {0x12345678, 0x9ABCDEF0}; uint32_t key[4] = {0xA1B2C3D4, 0x5E6F7A8B, 0x9C0D1E2F, 0x3A4B5C6D}; printf("Original: 0x%08X 0x%08X\n", data[0], data[1]); xxtea_encrypt(data, 2, key); printf("After encrypt: 0x%08X 0x%08X\n", data[0], data[1]); xxtea_decrypt(data, 2, key); printf("After decrypt: 0x%08X 0x%08X\n", data[0], data[1]); return 0; }
С таким вот выводом:
Original: 0x12345678 0x9ABCDEF0 After encrypt: 0xB9F221ED 0x71F80F5B After decrypt: 0x12345678 0x9ABCDEF0
Если кому то надо, то на моей системе такой результат времени запуска бинарника:
Original: 0x12345678 0x9ABCDEF0 After encrypt: 0xB9F221ED 0x71F80F5B After decrypt: 0x12345678 0x9ABCDEF0 ________________________________________________________ Executed in 3.96 millis fish external usr time 1.09 millis 1.09 millis 0.00 millis sys time 2.89 millis 1.02 millis 1.87 millis
❯ Прямой доступ к битам дробного числа
В языке C нет прямого способа посмотреть битовое представление числа с плавающей запятой. Трюк в том, чтобы заставить память интерпретироваться по-разному. Для этого используется union — область памяти, в которой данные разных типов хранятся по одному и тому же адресу.
#include <stdio.h> #include <stdint.h> typedef union { float f; struct { uint32_t mantissa : 23; uint32_t exponent : 8; uint32_t sign : 1; } bits; } float_cast; int main() { float_cast fc; fc.f = -3.14f; printf("Number: %f\n", fc.f); printf("Sign: %u\n", fc.bits.sign); printf("Exponent: %u\n", fc.bits.exponent); printf("Mantissa: 0x%06X\n", fc.bits.mantissa); return 0; }
Объявляется union с именем float_cast. Внутри него два поля: обычное число float f и структура bits. Они занимают один и тот же участок памяти размером 4 байта (32 бита). Когда мы записываем значение в fc.f, биты сразу располагаются в соответствии со стандартом IEEE 754. Через структуру bits мы получаем доступ к этим же битам, но уже как к отдельным именованным полям: знак (1 бит), порядок (8 бит) и мантисса (23 бита). Это не создаёт копии и не конвертирует данные — это просто другая «маска» для просмотра тех же битов.
Порядок полей в структуре зависит от архитектуры. В данном случае предполагается, что знаковый бит находится в старшем разряде.
Number: -3.140000 Sign: 1 Exponent: 128 Mantissa: 0x48F5C3
❯ Свертка массива XOR-ом для быстрой проверки изменений
Код выполняет быструю свёртку (контрольную сумму, checksum) массива данных с помощью операции XOR. Он создаёт короткий «отпечаток» массива, который резко меняется при любом изменении входных данных.
Главный принцип алгоритма в его чувствительности к изменениям. Изменится хотя бы один бит в любом элементе массива — и итоговый хэш, скорее всего, будет совершенно другим. Это делает метод идеальным для быстрой проверки целостности данных в памяти или при простом обмене, где не нужна криптографическая стойкость. Скорость работы очень высока, так как это всего один проход по данным с минимальной арифметикой.
#include <stdio.h> #include <stdint.h> uint32_t xor_fold(const uint32_t* data, size_t len) { uint32_t result = 0; for (size_t i = 0; i < len; i++) result ^= data[i]; return result; } int main() { uint32_t array1[] = {0x12345678, 0x9ABCDEF0, 0x13579BDF}; uint32_t array2[] = {0x12345678, 0x9ABCDEF0, 0x13579BDF}; uint32_t array3[] = {0x12345678, 0x9ABCDEF0, 0x13579BDE}; printf("Hash array1: %08X\n", xor_fold(array1, 3)); printf("Hash array2: %08X\n", xor_fold(array2, 3)); printf("Hash array3: %08X\n", xor_fold(array3, 3)); return 0; }
И вот такой вот вывод:
Hash array1: 9BDF1357 Hash array2: 9BDF1357 Hash array3: 9BDF1356
Он не заменит криптографический хэш, но отлично справляется с задачей быстрого обнаружения различий в блоках данных.
❯ ГПСЧ Tyche
Tyche — это вариация генератора Xorshift с 128-битным состоянием (мы его уже рассматривали в прошлой статье). Алгоритм использует сдвиги и XOR для перемешивания. Инициализация выполняет «прогрев» генератора (20 пустых итераций) для улучшения начального состояния. Генератор быстр и подходит для симуляций.
#include <stdio.h> #include <stdint.h> #include <time.h> typedef struct { uint32_t a, b, c, d; } TycheState; uint32_t tyche_next(TycheState *s) { uint32_t t = s->b; s->b = s->c; s->c = s->d; s->a = (s->a ^ (s->a << 11)) ^ (t ^ (t >> 19)); s->d = s->a ^ s->b ^ s->c; return s->d; } void tyche_init(TycheState *s, uint32_t seed) { s->a = 0x6C078965 * (seed ^ (seed >> 30)) + 1; s->b = s->a + 0x6C078965; s->c = s->b + 0x6C078965; s->d = s->c + 0x6C078965; for (int i = 0; i < 20; i++) { tyche_next(s); } }
Ключевая структура — TycheState. Это сердце генератора, где хранится его 128-битное состояние в виде четырёх 32-битных целых чисел: a, b, c, d.
Алгоритм работает в два этапа. Первый — инициализация в tyche_init. Она превращает один сид в начальное состояние из четырёх чисел. Используется умножение на константу и XOR для рассеивания битов. После этого генератор «прогревается» 20 холостыми тактами, чтобы устранить возможные корреляции, если начальное зерно было таким себе.
Второй этап — генерация нового числа в tyche_next. Это и есть вариация Xorshift. Алгоритм выполняет быструю перестановку: b, c, d сдвигаются, а значение a интенсивно перемешивается с использованием сдвигов и операции XOR с предыдущим значением b. Новое значение d (которое и возвращается) вычисляется как XOR всех трёх обновлённых регистров состояния. Весь процесс крайне быстр, требует лишь несколько битовых операций на процессоре.
И давайте реализуем пример работы:
int main() { TycheState rng; tyche_init(&rng, time(NULL)); printf("рандомные 16битные числа:\n"); for (int i = 0; i < 10; i++) { printf("0x%08X\n", tyche_next(&rng)); } printf("\nВ диапазоне 0..99:\n"); for (int i = 0; i < 10; i++) { printf("%d ", tyche_next(&rng) % 100); } printf("\n"); return 0; }
С вот таким выводом у меня:
рандомные 16битные числа: 0xF01D2505 0x71012006 0xE0182024 0x21292603 0x81310620 0x00002403 0xC0210425 0x81302026 0x88000000 0x89200004 В диапазоне 0..99: 38 20 16 84 72 61 80 52 44 65
❯ Заключение
Спасибо за прочтение статьи! Я надеюсь, вы узнали что‑нибудь новенькое, или, может быть, какой‑нибудь трюк натолкнул вас на другой интересный алгоритм. Если нашли нюанс в самой статье — пишите в комментарии.
Девятая часть получилась насыщенной: от сортировок и криптографии до битовой магии и математических трюков.
Если вам понравился изложенный материал, могу предложить вам подписаться на мой блог в телеграме. Если, конечно, вам статья понравилась и вы хотите видеть чуть больше.
Примеры работы кода вы можете увидеть в моем репозитории The Art Of Fun C.

Перед оплатой в разделе «Бонусы и промокоды» в панели управления активируйте промокод и получите кэшбэк на баланс.
Комментарии (5)

CitizenOfDreams
06.03.2026 07:02Слон — Bishop (епископ).
Или juicer (соковыжималка), как его называют диванные гроссмейстеры с подачи Накамуры.

alvoskov
06.03.2026 07:02О gjf64: никогда не слышал такого названия. Интересно, где именно Вы его встретили? Этот алгоритм, похоже, больше известен под названием xorshift128+ и взят отсюда: https://vigna.di.unimi.it/ftp/papers/xorshiftplus.pdf
О Tyche: то, что публиковалось под этим названием (https://link.springer.com/chapter/10.1007/978-3-642-31464-3_10) - это не LFSR, а нелинейный генератор на основе преобразования из ChaCha. У Вас же действительно какая-то модификация xorshift. Но откуда именно тогда были взяты параметры?

neurocore
06.03.2026 07:02В шахматах использование 64-битных чисел и правда классика, но не думаю что получение битбоарда всех коней как-то полезно. Обычно для известного индекса поля достают готовый битбоард из таблиц.
И ещё опечатка тут:for(intcol = 0; col < 8; col++) {3А вообще, было бы интересно послушать подробное объяснение алгоритма Kogge-Stone (я лично не въехал), полагаю, именно он здесь и использовался
LAutour
Статья про алгоритмы. Причем тут тайны С?
DrArgentum Автор
Алгоритмы реализованы на C. Но да, понимаю, серия статей уже исхудала себя, так что надо будет либо менять формат либо фокусироваться на си. Спасибо!