Компилятор Zig умеет на этапе компиляции собирать «структуру массивов» (SoA) из обычной структуры данных. За это отвечает контейнер MultiArrayList из стандартной библиотеки. Вместо массива структур (AoS) он хранит поля отдельными массивами — это классический приём для игровых движков, научных расчётов и компиляторов, где важна скорость работы с кэшем.
Разница в памяти наглядная. Допустим, есть структура Token с двумя полями: перечисление kind на 8 байт и слайс data на 16. Массив из 100 таких структур занимает 2400 байт. А MultiArrayList ужимает это до 1700: 1600 байт уходит на слайсы data, и всего 100 — на kind. В статье это проверяют тестом с фиксированным буфером на 1704 байта.
Как Zig вообще умеет генерировать такой тип? Всё дело в comptime (выполнение на этапе компиляции) и рефлексии. Типы в Zig — это обычные значения времени компиляции. Их можно передавать в функции, возвращать из них и даже собирать на лету. В статье показывают минимальный пример — FixedArrayList, который просто оборачивает слайс произвольного типа T.
Дальше — сложнее. Чтобы сделать SoA, нужно залезть внутрь типа: узнать список его полей, их типы и выравнивание. Для этого есть @typeInfo. В статье строят динамический тип PointN — точку с произвольным числом координат (x1, x2, x3...). Поля генерируются в цикле с помощью @Type и массива структур StructField. Выравнивание подхватывается через @alignOf.
Внутри MultiArrayList всё честно: он выделяет один большой byte-массив, сортирует поля по убыванию выравнивания и хранит смещения для каждого слайса. Код, несмотря на обилие низкоуровневой работы с указателями и индексами, в статье называют «довольно читаемым».
У подхода есть ограничение: динамически генерировать методы для такого типа пока нельзя (в отличие от полей). Но это косяк текущего API рефлексии, а не принципиальная проблема. Главный бонус — не нужно учить отдельный язык для макросов вроде proc macros из Rust. Достаточно разобраться с рефлексией Zig.