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

Меня зовут Женя Ерохин, у меня около 15 лет опыта разработки драйверов на C++ под macOS. Сейчас я старший разработчик в Kaspersky Lab, занимаюсь операционной системой KasperskyOS. Много всего создал в ее микроядре, а теперь знакомлю нашу ось со всяким железом на мобильных устройствах.

В этой статье я покажу, как мы использовали C++ при разработке драйвера для микроядерной ОС. В том числе разберу гетерогенный lookup — возможность выполнять поиск по разным типам аргументов без лишних преобразований. На практике такая фича хорошо показывает, почему C++ может быть полезен даже там, где обычно ждут C. Я расскажу, почему драйверы на C++ для микроядерной ОС — вполне рабочее инженерное решение. Покажу, где именно плюсы помогают писать более аккуратный и надежный код, как они упрощают работу со структурами данных и почему в нашей задаче такой выбор оказался естественным.

Задача: создать драйвер Vsock для микроядерной ОС

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

Контекст про KasperskyOS

Это собственная разработка «Лаборатории Касперского» на основе нашего микроядра. Написана она с нуля: никакой нарезки кусочков Linux тут нет.

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

На схеме выше показано классическое монолитное ядро: Linux, Windows, macOS. На самом нижнем уровне HAL, Hardware Abstraction Level: небольшая прослойка, которая позволяет сделать некоторые абстракции поверх самого низкого уровня железа. Контроллеры прерываний и подобные вещи, которые лежат практически рядом с самим ядром CPU.

Там же, в ядре, находятся все драйверы дисков, сетевых устройств, USB, таймеры и так далее. На одном уровне с ними разные подсистемы ядра: управление созданием тасков, тредов, VMM — виртуальная память, планировщик, IPC. В ядре находятся и все сетевые подсистемы. В защищенном режиме оказывается огромное количество кода, который при ошибке может выбить у соседа землю из-под ног.

Микроядерный подход устроен иначе. На этой схеме — классическое микроядро, как его описывает Эндрю Таненбаум. В ядре остаются Hardware Abstraction Level, таймер для прерывания процессов и IPC, чтобы один процесс мог отправить другому сообщение. Ядро контролирует, кто кому и что посылает.

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

В современных микроядрах, в том числе в KasperskyOS, красная линия между ядром и пользовательским пространством все же повыше. У нас в ядре остается управление задачами и процессами, управление виртуальной памятью, планировщик. Так просто эффективнее с точки зрения современного железа, которое по-прежнему затачивается под нужды монолитных ядер. Но при этом огромное количество кода — драйверы, файловые системы, сетевая подсистема — вынесено наружу.

Итак, мне нужно было написать для ОС драйвер VirtIO Vsock. VirtIO — стандарт, который используют гипервизоры, чтобы связывать гостевую систему с хостом. Например, предоставлять виртуализацию графического интерфейса, устройств ввода-вывода и в принципе пробрасывать в гостевую машину разное железо.

Vsock — одна из подсистем VirtIO. Она позволяет создавать сокеты для того, чтобы связывать гостевую машину с гипервизором, например передавать ей запросы, команды и другие данные.

Гипервизор общается с хостом при помощи PCI Express. А PCI Express — это не только слоты на материнской плате, соединенные металлическими дорожками, это еще и протокол. Работает он примерно так: есть определенная память, которую видит контроллер PCI Express. Мы пишем в эту память и читаем из нее — это транзакции с контроллером PCI Express. Получается довольно эффективная передача данных как с железом, так и с гипервизором.

Визуально это общение можно отразить в такой схеме:

Драйвер тут обрисован крупными мазками, но в нем есть три большие сущности. Vsock знает про протокол PCI Express и умеет по этому протоколу обмениваться данными с гипервизором. Есть биндинги, потому что протокол очень похож на TCP/IP, есть набор портов и аналог IP-адреса — так называемый CID. И есть сущность, которая обрабатывает входящие пакеты после установки соединения.

То есть на основе этого протокола можно сделать почти что полносвязную сеть, а в теории даже сделать несколько гипервизоров. Избыточно, конечно, но протокол позволяет.

На уровне пользования, если мы используем POSIX-интерфейс, все выглядит как обычный TCP/IP-сокет, доступны все те же функции. Сокет создается вызовом socket, а в качестве address family передается AF_VSOCK: так мы сообщаем машине, что создаем именно сокет Vsock.

Изначально менеджмент попросил меня реализовать этот Vsock-драйвер на C. Я начал писать и, конечно, задействовал любимую всеми сишниками структуру list, он же двусвязный список. Что я про него могу сказать? Наверное, это одна из самых отвратительных структур данных для современных процессоров. Основная проблема здесь — cache locality: элементы списка могут лежать в памяти где угодно, поэтому процессору приходится постоянно прыгать по адресам вместо того, чтобы эффективно читать данные последовательно.

Мы кладем структуры в список. Ноды двусвязного списка разбросаны черт знает где, а проход по списку практически всегда вымывает кэши. В результате все становится дико медленным. Это будет затормаживать необязательно только этот код: он, скорее всего, будет дополнительно вымывать кэши и соседям (новые линии кэша для наших нужд, надо у кого-то отобрать).

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

Я прокрастинировал свое недовольство list и вернулся к попыткам написать драйвер. У меня есть сущность Vsock. В ней мне нужно было хранить два двусвязных списка: один — для биндингов, второй — для соединений. Поэтому в саму структуру Vsock добавляются две головы списков: Binds и Connections.

Дальше каждый объект, который мы хотим поместить в такой список, должен содержать внутри себя ноду этого списка. В случае соединений я добавляю такое поле в структуру VsockConnection: оно позволяет включить конкретный объект соединения в список Connections.

Примерно так это выглядит в коде:

//Объявляем поле которое будет головой списка
struct Vsock {
   struct rtl_list_head binds;
   struct rtl_list_head connections;

//В структуре помещаемой в лист нужно добавить поле
struct VsockConnection {
   struct rtl_list_head connections;

//Итерируемся по списку
VsockConnection* it;
rtl_list_for_each_entry(it, &mgr->connections, connections) {
    if (it->localPort == localPort && it->remotePort == remotePort …

Делается это, естественно, на макросах. Есть define, в нем навешена какая-то логика. Чтобы итерироваться, я создаю переменную для итерирования, указываю макросу голову двусвязного списка и сообщаю, как называется поле в этой структуре. В нашем случае —Connections.

Внутри — нестандартная магия компилятора, которая позволяет получить смещение поля внутри структуры. И все это «замечательно» работает. Кавычки, потому что минус очевиден сразу: можно легко указать вообще не то название, а ошибку допустить проще простого.

Но это, конечно, не финал: мне еще нужны счетчики ссылок для объектов. Допустим, в одном треде мы решили начать работать с Connection, а в соседнем кто-то другой тут же решил этот Connection закрыть. Земля уходит из-под ног, все обваливается.

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

Я помучился денек, пытаясь очеловечить этот код и не дать ему уехать в дебри сложности, и понял, что пора идти ва-банк.

А давайте на плюсах

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

Использовать C++ для драйверов — не новая история. Например, на macOS большая часть драйверов для девайсов написана на плюсах. Правда, там есть ограничение: для драйверов можно использовать только ядро языка и, может быть, зацепить частично какую-нибудь стандартную библиотеку. В общем, очень небольшой кусок шаблонов.

В моем случае, поскольку KasperskyOS микроядерная, можно было использовать всю полноту языка. Драйвер — это обычный процесс, соответственно, мне доступны стандартная библиотека C++, std, исключения, контейнеры и так далее. В целом доступен вообще любой язык, хоть Python. Практического смысла в этом нет, но сам микроядерный подход такое позволяет. Так что я переписал драйвер C на C++. Новая структура выглядит так:

В ней остались все те же сущности, никуда не делся гипервизор, никуда не делся PCI Express. Но теперь взаимодействие обеспечено иначе: коллекции Connection и Binding хранятся в unordered_set. Причем хранятся там не просто указатели, а shared pointer. Так я решил проблему с подсчетом ссылок. И, естественно, у Connection есть еще один shared pointer на Binding, потому что соединение должно удерживать свой порт, чтобы его никто не забрал.

Unordered_set — это хеш-таблица. Соответственно, мне для нее нужны хеш-функция и функция сравнения. Когда в таблицу помещается объект, для него вычисляется хеш-функция. По нему становится понятно, к какой ячейке объект относится.

Одно из свойств хеш-таблицы — коллизии. Если они происходят, объекты с одинаковым хешем организуются в связный список. Но тысячи соединений встречаются редко, а значит, коллизии не становятся постоянной проблемой.

Вот как определен контейнер unordered_set в стандарте:

std::unordered_set
template<
    class Key,
    class Hash = std::hash<Key>,
    class KeyEqual = std::equal_to<Key>,
    class Allocator = std::allocator<Key>
> class unordered_set;

Здесь есть ключ, то есть тип, который нужно хранить. У меня это shared pointer. Есть сущность, которая будет генерировать хеши и, естественно, сравнение. Аллокатор использован стандартный.

Но от языка хочется получить больше комфорта. Иногда нужно искать не по shared point. Например, пришел по PCI Express пакет, у него есть заголовок, и я хочу узнать у Connection, чей это пакет.

А иногда нужно получать указатель на сам Connection: вот я поработал с соединением, потом его закрываю. У unordered_set, как и у многих других контейнеров, закрытие идет по итератору. Чтобы его получить, нужно находить ссылку на сам объект Connection.

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

Что представляет собой моя хеш-функция

Это структура, в которой реализованы операторы вызова. Внутри есть базовая функция hashBase: она берет три значения, по которым соединение можно однозначно найти, — номер локального порта, номер удаленного порта и CID, внешний виртуальный адрес. Из них и собирается хеш.

Покопавшись в интернетах, я нашел вот такой вариант компоновки хеш-функций.

struct ConHash
   {
 
       static std::size_t hashBase(vsock_port_t localPort, vsock_port_t remotePort, vsock_cid_t remoteCid) noexcept {
           auto h1 = std::hash<vsock_port_t>{}(localPort);
           auto h2 = std::hash<vsock_port_t>{}(remotePort);
           auto h3 = std::hash<vsock_cid_t>{}(remoteCid);
           return h1 ^ (h2 << 1) ^ (h3 << 2);
       }
       std::size_t operator ()(const std::shared_ptr<Connection>& con) const noexcept {
           return hashBase(con->localPort(), con->remotePort(), con->remoteCid());
       }
};

Логика простая: берем отдельные std::hash от отдельных параметров, а потом нехитрым смещением и XOR объединяем их в одно хеш-значение.

Первый нужный оператор — для shared_ptr. С ним из указателя на Connection вытаскиваются локальный порт, удаленный порт и CID, затем все это передается в hashBase, а на выходе получается готовый хеш для контейнера.

Но комфорт начинается дальше. Я добавляю еще пару перегрузок:

}
       std::size_t operator ()(const Connection& con) const noexcept {
           return hashBase(con.localPort(), con.remotePort(), con.remoteCid());
       }
       std::size_t operator ()(const VirtioVsockHdr& hdr) const noexcept  {
           return hashBase(hdr.dstPort, hdr.srcPort, hdr.srcCid);
       }

Теперь хеш можно посчитать не только для shared_ptr<Connection>, который хранится в контейнере, но и для самого Connection, и для заголовка входящего пакета.

В случае с Connection все тривиально: передаем ссылку, вытаскиваем из объекта нужные значения и отправляем их в hashBase.

С заголовком пакета почти то же самое. Только значения берутся уже из полей заголовка: dstPort, srcPort и srcCid. То есть пришел пакет — я могу сразу посчитать по нему тот же самый хеш, по которому в контейнере лежит нужное соединение.

Оператор сравнения я реализовал примерно по той же логике: отдельно для shared_ptr, отдельно для Connection, отдельно для заголовка пакета. Казалось бы, теперь все должно работать: контейнер хранит shared_ptr<Connection>, но искать я могу и по самому соединению, и по заголовку входящего пакета.

Пытаюсь скомпилировать — и получаю вот такую радость:

error: no matching member function for call to 'find'
  88 |     auto it = m_connections.find(hdr);
     |               ~~~~~~~~~~~~~~^~~~
include/c++/v1/unordered_set:884:20: note: candidate function not viable: no known conversion from 'const VirtioVsockHdr' to 'const key_type' (aka 'const std::shared_ptr<vsock::Connection>') for 1st argument
 884 |     iterator       find(const key_type& __k)       {return __table_.find(__k);}
     |                    ^    ~~~~~~~~~~~~~~~~~~~
include/c++/v1/unordered_set:886:20: note: candidate function not viable: no known conversion from 'const VirtioVsockHdr' to 'const key_type' (aka 'const std::shared_ptr<vsock::Connection>') for 1st argument
 886 |     const_iterator find(const key_type& __k) const {return __table_.find(__k);}
     |                    ^    ~~~~~~~~~~~~~~~~~~~
include/c++/v1/unordered_set:890:20: note: candidate template ignored: requirement '__is_transparent<vsock::ConnectionManager::ConHash, VirtioVsockHdr, void>::value' was not satisfied [with _K2 = VirtioVsockHdr]
 890 |     iterator       find(const _K2& __k)            {return __table_.find(__k);}

Я передал в find заголовок пакета, а контейнер все равно отказался искать. Он перебрал несколько вариантов и объяснил, почему каждый из них ему не подходит.

Первый вариант:

include/c++/v1/unordered_set:884:20: note: candidate function not viable: no known conversion from 'const VirtioVsockHdr' to 'const key_type' (aka 'const std::shared_ptr<vsock::Connection>') for 1st argument
 884 |     iterator       find(const key_type& __k)       {return __table_.find(__k);}
     |                    ^    ~~~~~~~~~~~~~~~~~~~

Это обычный find, который принимает key_type. То есть ровно тот тип, который хранится в контейнере. В моем случае это std::shared_ptr<vsock::Connection>. А я пытаюсь передать VirtIOVsockHdr. Естественно, такой конверсии нет.

Второй вариант — почти то же самое, только константная версия поиска:

include/c++/v1/unordered_set:886:20: note: candidate function not viable: no known conversion from 'const VirtioVsockHdr' to 'const key_type' (aka 'const std::shared_ptr<vsock::Connection>') for 1st argument
 886 |     const_iterator find(const key_type& __k) const {return __table_.find(__k);}
     |                    ^    ~~~~~~~~~~~~~~~~~~~

Тут контейнер снова пытается искать по дефолтному ключу, то есть по типу, который я указал для unordered_set.

А вот третий вариант уже интереснее:

include/c++/v1/unordered_set:890:20: note: candidate template ignored: requirement '__is_transparent<vsock::ConnectionManager::ConHash, VirtioVsockHdr, void>::value' was not satisfied [with _K2 = VirtioVsockHdr]
 890 |     iterator       find(const _K2& __k)            {return __table_.find(__k);}

Здесь компилятор фактически говорит: «Я бы мог поискать по другому типу, но у тебя не выполнено требование». И требование это — is_transparent для хешера. Главное слово тут — not satisfied.

Вот здесь и лежит ответ, как проблему победить. Нужно внутри хешера объявить специальный тип is_transparent. Тип может быть любым: void, int, что угодно. Важен сам факт, что такое имя есть.

struct ConHash
{
    using is_transparent = void;

    static std::size_t hashBase(
        vsock_port_t localPort,
        vsock_port_t remotePort,
        vsock_cid_t remoteCid
    ) noexcept {
        ...
    }
};

При наличии такого имени внутри структуры все внезапно начинает работать.

А почему так?

Если заглянуть в исходники unordered_set, там есть такая конструкция:

#if _LIBCPP_STD_VER >= 20
   template <class _K2, enable_if_t<__is_transparent<hasher, _K2>::value && __is_transparent<key_equal, _K2>::value>* = nullptr>
   _LIBCPP_INLINE_VISIBILITY
   iterator       find(const _K2& __k)            {return __table_.find(__k);}
…

Первая строчка — проверка версии стандарта. Дальше как раз начинается гетерогенный lookup: возможность искать значение в контейнере не только по его родному ключу, но и по другому типу, если этот тип поддержан функцией хеширования и функцией сравнения.

В функции появляется параметр K2. То есть это уже не тот K-type, который был раньше. Фактически find может получить любой тип.

Чуть выше в описании шаблона видно второе важное условие — enable_if. Оно разрешает эту перегрузку только в том случае, если выполнены предикаты: is_transparent есть у хешера и is_transparent есть у функции сравнения.

Для std::set, std::map, std::multiset и std::multimap гетерогенный lookup стандартизировали еще в C++14. А вот для unordered_set, unordered_map и остальных unordered-контейнеров эта функциональность появилась только в C++20.

Тип 

Ordered Containers (C++14+)

Unordered Containers (C++20+)

Set

std::set,
std::multiset

std::unordered_set, std::unordered_multiset

Map

std::map,
std::multimap

std::unordered_map, std::unordered_multimap

С хеш-функцией разобрался, теперь функция сравнения. А она, по сути, эквивалентна:

struct ConEqual
   {
       using is_transparent = void;
       bool operator ()(const std::shared_ptr<Connection>& lv, const std::shared_ptr<Connection>& rv) const noexcept
       {
           return *lv == *rv;
       }
       bool operator ()(const std::shared_ptr<Connection>& lv, const Connection& rv) const noexcept
       {
           return *lv.get() == rv;
       }
       bool operator ()(const std::shared_ptr<Connection>& lv, const VirtioVsockHdr& hdr) const noexcept
       {
           return lv->localPort() == hdr.dstPort
               && lv->remotePort() == hdr.srcPort
               && lv->remoteCid() == hdr.srcCid;
       }
   };

Здесь я тоже указываю is_transparent, чтобы включить гетерогенный lookup. А дальше реализую простые операции сравнения: для двух shared_ptr, для shared_ptr и самого Connection, для shared_ptr и заголовка пакета.

Первые две операции я дополнительно реализовал внутри самого Connection, чтобы ими можно было пользоваться и в других местах.

Теперь тот самый пример, который я изначально пытался собрать:

// Поиск по заголовку пакета
   auto it = m_connections.find(packet->header);
   if (it == m_connections.end())
       return nullptr;
   Connection* con = it->get();
…
   //Если connection нам уже не нужен
   Connection* con = …;
   m_connections.erase(m_connections.find(*con));

Есть входящий пакет. Я просто передаю в find его заголовок и проверяю, есть ли в контейнере нужное соединение. Мне даже не надо помнить, по каким полям оно должно сравниваться в месте использования. В C-коде я бы руками сравнивал локальный порт, удаленный порт, CID. Здесь я просто отдаю контейнеру заголовок пакета, а вся логика поиска уже описана в одном месте.

Когда Connection после работы больше не нужен, я так же спокойно его закрываю. Erase для unordered_set работает через итератор, а итератор я получаю через find(*con). Поскольку я только что работал с этим соединением, то знаю, что оно есть в контейнере. Все находится и закрывается в полторы строчки.

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

std::set<std::string> myset;
…
   auto it = myset.find("Something");

Стандартный find принимает тип исходного ключа, то есть тот тип, который хранится в контейнере. Допустим, есть std::set<std::string>, в котором мы хотим найти ascii-строку.

Законное желание, но поскольку у find тип K, он сперва посмотрит, что у set:String есть конверсия из ascii-строк, то есть указателя на char string. Потом создаст временный объект std::string, скопирует туда строку и уже по этому объекту начнет поиск.

А если передать сравнение с включенным is_transparent, временный объект создавать не нужно. Включится find, который принимает произвольный тип, а std::string уже умеет сравниваться с const char*. В итоге можно выполнить поиск без лишнего копирования.

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

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

Заключение

Для меня в этой задаче C++ в очередной раз показал себя как действительно системный язык. Именно перетаскивая драйвер с C на C++, я узнал от компилятора про существующие баги. Си это не интересует, он скорее оперирует в парадигме: «Раз передают, то знают, что делают, не буду мешать».

А вот C++ необязательно требует платы производительностью. В низкоуровневом коде он может работать на том же уровне, что и C, а иногда даже давать более эффективный результат без танцев с бубном: за счет более удачных абстракций и оптимизаций компилятора.

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

Если вы привыкли думать, что рядом с ядром место только C, к C++ все же стоит приглядеться. А если уже пишете на плюсах — попробуйте гетерогенный lookup. Возможно, он не изменит вашу жизнь, но пару неприятных мест в коде точно сделает проще, чище и быстрее. А если вам интересны инженерные развилки, описанные выше, — у нас в Kaspersky таких задач много. Заглядывайте, возможно, найдете что-то близкое вам.

Комментарии (11)


  1. segment
    04.09.2026 13:30

    "проще, чище" — серьёзно?


    1. Sixshaman
      04.09.2026 13:30

      > Проще, чище и быстрее, чем на C

      > Looks inside

      > std::unordered_set<std::shared_ptr<T>>


    1. SilverTrouse
      04.09.2026 13:30

      Вы не поверите, но да. C++ обладает высоким уровнем абстракции который позволяет удобно работать с низкоуровневыми вещами не платя за это высокую цену.

      PS

      Если вы из секты свидетелей смерти с++. то прошу к остальным, думавшим так же . А пока С++ все еще живее всех живых и развивается успешно


      1. segment
        04.09.2026 13:30

        Я ничего не говорил про С++ в целом. Приведенный пример в статье не выглядит "проще, чище", вот и всё.


  1. SilverTrouse
    04.09.2026 13:30

    Опа выложили доклад с зимнего митапа.


  1. peschex0d
    04.09.2026 13:30

    Без листинга скомпилированного кода утверждение, что фича сделала драйвер "проще, чище и быстрее" выглядит голословно.

    Вполне допускаю, что комфорт разработчика растет с применением шаблонов c++, но в обсуждаемом контексте смущает момент хранения данных, вызвавших коллизии хеша, в списках. На гитхабе в репозитории Nicoshev/rapidhash приведены оценки размера исполняемого кода в примерно 140 инструкций x86-64 и aarch64 - это не слишком дорого за возможность удобно и комфортно по соединению узнать порт и, наоборот, по порту узнать его соединение? Это лишь стоимость вычисления хеш функции (одной из), без накладных расходов на розыск в списках при обнаружении коллизий.

    Безотносительно хешей, списки никто не мешает хранить в обычном массиве в буквально смежной области памяти, помещающейся в кеш процессора.

    Тут, наверное, имеет смысл иллюстрировать подробнее, сколько и каких данных использует "драйвер", что вдруг хеширование стало философским камнем.

    И, повторюсь, код, сгенерированный "под капотом", интересует очень.

    При написании дизассемблера мне нужно было хранить адреса процедур и переходов и иметь возможность проверять, ссылается ли кто-нибудь на конкретный адрес. Для сегмента кода размером 64 кбайт, если помечать использованные для перехода или вызова адреса одним битом, достаточно массива из 4кбайт 16-разрядных int. Без поиска перебором, без хеширования, без коллизий и т.д. и в пару команд процессора всего. Это, безусловно, частный случай, но и иллюстрация KISS принципа.


    1. 0hrenet
      04.09.2026 13:30

      Без поиска перебором, без хеширования, без коллизий и т.д. и в пару команд процессора всего.

      Ну, строго говоря, это тоже хеширование. Но без коллизий, 1:1.