Всем привет!

Под прошлой статьёй я пообещал показать бенчмарки nginx.exe. Так вот — показываю!

Бенчмарк №0 (затравочка)
Бенчмарк № 0 (затравочка)

Но прежде чем перейти к подробному разбору бенчмарков, расскажу об устройстве Vodka и покажу, как мы получаем оригинальные файлы Windows, не нарушая лицензий.

Чем же мы занимаемся?

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

Vodka можно разделить на четыре слоя, и выглядеть это будет так:

Слои Vodka
Слои Vodka

Программы в пространстве пользователя (apps)

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

Библиотеки прослойки (os-proxy)

Сюда входят файлы, которые мы даём программам под видом их зависимостей. При этом их список довольно небольшой, так как мы подменяем самые низкоуровневые библиотеки пространства пользователя, которые являются API‑обёртками над системными вызовами к ядру ОС. Мы грамотно и изящно реализуем это API, опираясь на возможности следующего ОС-независимого слоя. Это сложная и муторная работа, но мы планомерно реализуем функцию за функцией, находя кандидатов по журналу необработанных вызовов при падениях очередной тестируемой программы. Сейчас реализовано 68,7% Nt-вызовов ntdll и 77,5% Nt-вызовов win32u.

Немаловажно добавить, что на этом же слое находится и Ke‑API, который нам необходим для запуска оригинальных драйверов и прочей «ядерной» работы.

Примитивы Vodka (vdk primitives)

Это самый важный слой, который позволяет нам выделить абстрактный, ОС‑независимый API к системе. Он разделён на несколько частей, и у каждой своя зона ответственности. Здесь располагаются компоненты: vdkldr — загрузка исполняемых файлов, vdkio — работа с файлами, vdksync — примитивы синхронизации, vdkgl — работа с графикой и другие.

Бэкенд примитивов (primitives backend)

Здесь мы описываем всё то, что является ОС‑спецификой, то есть как и с чем будут работать примитивы. Простой пример: в backend-части vdkio_open() использует open() на Linux и NtOpenFile() на Windows. Мы выбрали Linux основной платформой и сейчас полноценно развиваем backend-часть именно для него.

Откуда взять оригинальные файлы и их зависимости?

Под прошлой статьей множество комментариев было посвящено юридическим вопросам.
И действительно, как Vodka может функционировать, если легально и без нарушения лицензий никак нельзя получить те самые библиотеки WinAPI: kernel32.dll, kernelbase.dll, user32.dll и множество других?
Так вот, в прошлой статье все зависимости, те самые файлы из директории winfiles, были получены с примонтированного диска с установленной Windows. И этот способ рабочий и легальный, но, будем честны, довольно неприятный для пользователя, так как требует много действий. А для нас, как для разработчиков, он не даёт необходимой воспроизводимости полученного результата, да и порой сложно определить, что именно нужно взять. И тут мы задумались и придумали достаточно, скажем так, радикальное решение.

А что, если установить Windows на Linux?

Логика проста: если мы не можем взять зависимости по одной — мы возьмём их все, следуя официальному пути распространения. Через установку образа Windows. И для этого у нас появилась отдельная утилита — vodka‑decant. Она автоматизирует получение зависимостей несколькими способами; один из них — создание миров через полноценную установку из ISO-образа.

Миры?

Под миром в контексте Vodka подразумевается определённая конфигурация окружения для запуска.

  • Откуда брать зависимости?

  • С какими опциями запускается vodka?

  • Где файлы реестра?

  • Чей это процесс?

  • Как видеть файловую систему?

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

Что внутри ISO-образа?

$ vodka-decant windows list --iso ~/Windows-11-25H2.iso
EDITION        SIZE  NAME
      1    20.7 GiB  Windows 11 Home
      2    20.1 GiB  Windows 11 Home N
      3    20.6 GiB  Windows 11 Home Single Language
      4    21.5 GiB  Windows 11 Education
      5    20.9 GiB  Windows 11 Education N
      6    21.6 GiB  Windows 11 Pro
      …
11 edition(s). `--edition N` says what is in one of them, and `import --edition N` takes it.

Здесь мы просто вывели список выпусков Windows, которые можно установить из образа. И возьмём мы шестой выпуск — Windows 11 Pro.

Как выглядит установка?

$ vodka-decant windows import --iso ~/Windows-11-25H2.iso --edition 6 --world win11
world 'win11' made at ~/.local/share/vodka/worlds/win11
windows: edition 6 of 11: Windows 11 Pro
…
laying the medium out at ~/.cache/vodka/decant/medium/Windows-11-25H2
  975 file(s), 7372 MiB laid out
laying the image down: C:\Windows\System32\Dism.exe /Apply-Image /ImageFile:D:\sources\install.wim /Index:6 /ApplyDir:T:\
…
Deployment Image Servicing and Management tool
Version: 10.0.26100.5074

Applying image
[==========================100.0%==========================]
The operation completed successfully.
…
the registry, out of the hives the image laid down:
  Windows/System32/config/SYSTEM           25666 key(s), 65528 value(s) -> Windows\Machine\SYSTEM
  Windows/System32/config/SOFTWARE         258578 key(s), 412931 value(s) -> Windows\Machine\SOFTWARE
  Windows/System32/config/SECURITY         1 key(s), 0 value(s) -> Windows\Machine\SECURITY
  Windows/System32/config/SAM              1 key(s), 0 value(s) -> Windows\Machine\SAM
  Windows/System32/config/DRIVERS          23924 key(s), 28090 value(s) -> Windows\Machine\DRIVERS
  Windows/System32/config/DEFAULT          64 key(s), 185 value(s) -> Windows\User\.DEFAULT
  …/targets/windows.unattend.xml -> Windows/Panther/unattend.xml
what the new machine runs before its setup: C:\Windows\System32\bcdboot.exe C:\Windows /s C: /f UEFI
…
Boot files successfully created.
booting the new machine through the end of its setup
world 'win11' finishing its setup
  setup: SystemSetupInProgress 1, OOBEInProgress 1, SetupType 1
boot 1
  …
  setup execute: setupcl.exe
  setupcl.exe            762987  session 0, …/setupcl.log
    exit 0
  wininit.exe            762993  session 0, …/wininit.log
  winlogon.exe           787449  session 1, …/winlogon.log
  after 6s
  setup: SystemSetupInProgress 1, OOBEInProgress 1, SetupType 0
  after 8s
  setup: SystemSetupInProgress 1, OOBEInProgress 1, SetupType 2
  after 114s
  setup: SystemSetupInProgress 0, OOBEInProgress 1, SetupType 2

Самое ценное, что мы здесь видим, — это строчка про DISM. Образ на диск кладёт не наш код, а Dism.exe — та самая программа, с помощью которой это делает установщик Windows. Главное — мы не имитируем установку, а выполняем настоящую, просто дав ей возможность отработать на нашем слое. Затем Windows настраивает себя сама, и мы получаем всё, что нужно.

Также важно обратить внимание на строку 23:
…/targets/windows.unattend.xml -> Windows/Panther/unattend.xml
Мы подкладываем специальный файл перед перезагрузкой и следующим этапом установки. Если вы с таким не сталкивались, поясню: этот файл содержит ответы на вопросы установки, на которые должен отвечать человек. Это штатный механизм, и его используют при автоматическом развёртывании.

Вот так выглядит этот XML
<unattend xmlns="urn:schemas-microsoft-com:unattend">
  <settings pass="oobeSystem">
    <component name="Microsoft-Windows-Shell-Setup"
               processorArchitecture="amd64"
               publicKeyToken="31bf3856ad364e35"
               language="neutral"
               versionScope="nonSxS"
               xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
      <OOBE>
        <SkipMachineOOBE>true</SkipMachineOOBE>
        <SkipUserOOBE>true</SkipUserOOBE>
        <HideEULAPage>true</HideEULAPage>
        <HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>
        <HideOnlineAccountScreens>true</HideOnlineAccountScreens>
        <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
        <ProtectYourPC>3</ProtectYourPC>
      </OOBE>
      <UserAccounts>
        <LocalAccounts>
          <LocalAccount wcm:action="add">
            <Name>vodka</Name>
            <Group>Administrators</Group>
            <DisplayName>vodka</DisplayName>
            <Password>
              <Value>vodka</Value>
              <PlainText>true</PlainText>
            </Password>
          </LocalAccount>
        </LocalAccounts>
      </UserAccounts>
      <AutoLogon>
        <Enabled>true</Enabled>
        <Username>vodka</Username>
        <LogonCount>999999999</LogonCount>
        <Password>
          <Value>vodka</Value>
          <PlainText>true</PlainText>
        </Password>
      </AutoLogon>
      <TimeZone>UTC</TimeZone>
    </component>
  </settings>
</unattend>

Что получилось после установки?

Теперь у нас есть вот такая директория, которая представляет мир:

$ tree -L1 ~/.local/share/vodka/worlds/win11
/home/ub/.local/share/vodka/worlds/win11
├── files           // его C:
├── registry.hiv    // его реестр — кусты, которые привёз образ
├── removed         // имена, которые он удалил
└── world.conf      // что мир о себе говорит

И сейчас создадим из мира win11 новый мир nginx:

$ vodka world new nginx --from win11
WORLD                        ENGINES  WHERE
nginx                        0        ~/.local/share/vodka/worlds/nginx

falls through to ~/.local/share/vodka/worlds/win11

И да, благодаря устройству миров форк занял 0,12 секунды.

Теперь сам nginx. Возьмём ветку Mainline — на момент написания статьи это версия 1.31.6. Официальная сборка для Windows — ZIP‑архив, и распаковывать его мы будем средствами самой Windows: в Windows 11 есть tar.exe. Директории с архивом на один запуск даём букву диска:

$ vodka --world nginx --map 'i"D:" => "/home/ub/Downloads"' \
        tar.exe -xf 'D:\nginx-1.31.6.zip' -C 'C:\'

$ vodka --world nginx 'C:\nginx-1.31.6\nginx.exe' -v
nginx version: nginx/1.31.6

$ vodka --world nginx 'C:\nginx-1.31.6\nginx.exe' -p 'C:\nginx-1.31.6' -t
nginx: the configuration file C:\nginx-1.31.6/conf/nginx.conf syntax is ok
nginx: configuration file C:\nginx-1.31.6/conf/nginx.conf test is successful
А вот карта подгружаемых зависимостей для nginx.exe
$ vodka --world nginx --print-map 'C:\nginx-1.31.6\nginx.exe' -v
-- loaded modules (engine at 0x000000005663c000) --
  0x0000000000400000 +0x676000   nginx.exe
  0x00000000737e2000 +0x107000   crypt32.dll
  0x000000007c62a000 +0x9000     dpapi.dll
  0x000000007393d000 +0x61000    ws2_32.dll
  0x00000000738e9000 +0x54000    mswsock.dll
  0x000000007fe1b000 +0x1c5000   user32.dll
  0x000000007fdf8000 +0x23000    gdi32.dll
  0x0000000073b33000 +0xec000    gdi32full.dll
  0x0000000074559000 +0x37000    DXCore.dll
  0x0000000073aae000 +0x85000    msvcp_win.dll
  0x000000007399e000 +0x110000   ucrtbase.dll
  0x00000000741b9000 +0x7f000    advapi32.dll
  0x000000007ffe2000 +0x14000    cryptsp.dll
  0x000000007fff6000 +0xa000     cryptbase.dll
  0x0000000073c1f000 +0xbc000    rpcrt4.dll
  0x0000000073cdb000 +0x83000    sechost.dll
  0x0000000073d5e000 +0xc7000    msvcrt.dll
  0x000000007c633000 +0x23d000   win32u.vdk
  0x0000000010000000 +0xe5000    kernel32.dll
  0x0000000073e25000 +0x2cb000   KernelBase.dll
  0x0000000074238000 +0x2c8000   ntdll.vdk
nginx version: nginx/1.31.6

И из этого набора только ntdll.vdk и win32u.vdk наши.

Запускаем со штатным конфигом и стучимся:

$ vodka --world nginx 'C:\nginx-1.31.6\nginx.exe' -p 'C:\nginx-1.31.6' &

$ curl -sD - http://127.0.0.1:63080/ | head -2
HTTP/1.1 200 OK
Server: nginx/1.31.6

$ vodka bar --world nginx
MARK  PROGRAM   WORLD STATE    PID     W   AGE       CPU       HELD
a676  nginx.exe nginx running  2412367 32  0:00:00   0:00:00   368M
e9cf  nginx.exe nginx running  2412418 32  0:00:00   0:00:00   149M

Вы можете заметить порт 63080 вместо 80, и это не опечатка. Linux по умолчанию отдаёт порты ниже 1024 только пользователю root, а в данном случае Vodka работает от обычного пользователя. Поэтому на стороне хозяина мир занимает порт из верхнего диапазона. Для nginx.exe и для всех программ мира это по‑прежнему порт 80: на вопрос о своём адресе nginx отвечает: «80», и соседи по миру находят его на 80. Снаружи, из Linux, к нему стучатся на 63080.

Цифры и бенчмарки!

Пора перейти к самому главному и сравнить производительность Vodka и Wine.

Что с чем сравниваем

  • Один и тот же файл. nginx.exe 1.31.6 — официальная сборка с nginx.org, 32-битная.

  • Wine в полной комплектации. Wine 11.16 из репозитория дистрибутива с ntsync в ядре. И отдельной строкой — тот же Wine без ntsync.

  • Для масштаба — nginx той же версии, но для Linux и 64-битный. Просто чтобы видеть потолок.

  • Конфиг один на всех: ответ в 18 байт, keep‑alive, журнал запросов выключен.

  • Нагрузка — wrk -t8 -c100, десять секунд после прогрева. Пять повторов, движки чередуются, в таблицах медианы.

  • Процессорное время на запрос считается по всем процессам сервера. У Wine в счёт входит wineserver: без него половина работы Wine осталась бы за кадром.

Что на одном воркере?

Бенчмарк №1 (один воркер)
Бенчмарк № 1 (один воркер)

В шесть раз по ответам в секунду. Почти в восемь — по процессорному времени. И 80% от nginx, собранного под Linux, притом что это программа для Windows на настоящих ws2_32.dll и mswsock.dll.

Почему?

Не потому, что мы что‑то хитро оптимизировали. Давайте просто посмотрим на путь одного запроса у Vodka и у Wine:

Пути запроса у Vodka и у Wine
Пути запроса у Vodka и у Wine

У Wine объекты ядра Windows — сокеты, события, описатели — живут в отдельном процессе, который называется wineserver. Чтобы прочитать запрос, nginx.exe сначала спрашивает сервер (recv_socket), потом читает сам и докладывает серверу, чем кончилось (set_async_direct_result). Чтобы ответить — то же самое ещё раз.

У нас сервера нет. Вообще. NtDeviceIoControlFile из подлинной mswsock.dll приходит в нашу ntdll.vdk, и та в том же потоке делает системный вызов Linux. А то, что должно быть общим для нескольких процессов, лежит в общей памяти мира, и процесс берёт это сам: свободный замок — одна инструкция процессора и ни одного системного вызова.

А теперь, вооружившись strace и /proc, посмотрим системные метрики:

Бенчмарк №2 (системные метрики)
Бенчмарк № 2 (системные метрики)

Наши 2,6 вызова — это recvmsg, sendmsg и десятая доля poll: те же три, что делает родной nginx, плюс часы. У Wine на тот же запрос 43, и делятся они почти поровну: 22 делает nginx.exe — пишет серверу, читает ответ, закрывает и открывает сигналы вокруг каждого обращения — и 21 сам wineserver.

Теперь посмотрите на предпоследнюю строку. В ядре Linux nginx.exe на Vodka проводит почти столько же, сколько родной nginx: 3,5 микросекунды против 3,2. Вся наша наценка — микросекунда с небольшим в пользовательском режиме: это настоящие библиотеки Windows и наш слой под ними. А у Wine в ядре 27 микросекунд — и это не сеть. Тут два процесса разговаривают друг с другом: почти половину процессорного времени запроса, 47%, тратит не nginx.exe, а сервер.

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

А что ntsync?

Когда я показал знакомому первые результаты, он сказал: «Конечно, в 6 раз! Поставь Wine с ntsync, он очень сильно ускоряет wineserver!»

Так вот, ntsync — модуль ядра Linux, который берёт на себя часть работы wineserver: ожидание мьютексов, семафоров и событий. Там, где программа ждёт именно их, например в играх, то да, он помогает очень заметно, и в Wine 11 это штатный режим.
Но nginx ждёт не мьютексов, а сокетов, — а сокеты как жили в сервере, так и живут. Смотрите в таблицу: обращений к серверу и переключений контекста с ntsync столько же, а системных вызовов больше — к прежним 43 сервер добавил шесть ioctl на запрос: теперь ему приходится держать в курсе ещё и модуль. В итоге Wine с ntsync здесь стабильно медленнее, чем без него: на 6% с keep‑alive, на 5% на новых соединениях.

Что на нескольких воркерах?

До сих пор у nginx был один рабочий процесс. Попросим больше: worker_processes 2, потом 4, 8 и 16. Клиент — wrk -t12 -c240, один и тот же на любое число воркеров; три повтора, в таблице медианы.

Бенчмарк №3 (несколько воркеров)
Бенчмарк № 3 (несколько воркеров)

На шестнадцати воркерах nginx.exe на Vodka отдаёт 1,13 миллиона ответов в секунду. На Wine — те же 23 тысячи, что и на одном. В целых 48 раз медленнее!
И причин тут две.

Какая первая причина?

nginx для Windows не передаёт воркерам слушающий сокет: каждый воркер открывает порт сам и просит разрешения занять уже занятый адрес. Под Wine второй воркер получает отказ. В error.log остаётся:

[emerg] 332#336: bind() to 127.0.0.1:8242 failed (10013: Access denied)
[alert] 296#300: worker process 332 exited with code 1

И nginx остаётся с одним воркером, сколько бы их ни просили.

А вторая причина?

Вторая интереснее. На настоящей Windows порт откроют все воркеры, но соединения достанутся одному — это прямо описано в документации nginx: «Although several workers can be started, only one of them actually does any work». У нас просьбу «разделить адрес» получает ядро Linux, а оно умеет раздавать соединения по всем сокетам, которые слушают порт. Работают все шестнадцать.

И выходит, программа для Windows получила то, чего у неё на самой Windows нет, и при этом осталась собой. Круто же!

А если серверов несколько?

Справедливое возражение: может, под Wine всё упирается только в этот bind()? Уберём из эксперимента модель воркеров nginx совсем. Запустим несколько отдельных nginx.exe, каждый на своём порту, и каждому дадим своего клиента. Под Wine все они живут в одном префиксе, у нас — в одном мире.

Бенчмарк №4 (несколько серверов)
Бенчмарк № 4 (несколько серверов)

Сначала Wine растёт уверенно. Два сервера — вдвое больше ответов. Но у всех этих серверов один wineserver, а он однопоточный. На четырёх серверах он занят на 92%, на восьми — на все 100%, и на этом рост кончается: 90 тысяч ответов в секунду — потолок на весь префикс, сколько бы у меня ни было ядер. У нас потолка нет, и восемь серверов идут на тех же 80% от родного nginx, что и один.

Итог

  • nginx.exe на Vodka отдаёт 139 тысяч ответов в секунду на одном воркере. Тот же файл на Wine 11.16 — 23 тысячи. В шесть раз, а по процессорному времени на запрос — почти в восемь.

  • На шестнадцати воркерах — 1,13 миллиона против тех же 23 тысяч. Восемь отдельных серверов — 771 тысяча против 90. Дальше Wine упирается в собственный сервер.

  • Причина не в оптимизации, а в устройстве: у нас между программой и ядром нет процесса‑посредника. 2,6 системных вызова на запрос против 43, одно переключение контекста на сотню запросов против восьми на каждый.

  • Файлы Windows в мир попадают через собственный установщик ОС, с образа пользователя. В поставке Vodka нет ни одного файла Microsoft, и код Microsoft мы не изменяем ни на байт.

  • Мир — это директория. Создание мира через наследование другого, даже с полноценно установленной Windows, — 0,12 секунды.

Мы не писали быстрый сервер объектов. Не писали ws2_32. Не писали установщик Windows. Мы сделали слой совместимости, тонкую границу — и всё, что над ней, заработало само.

Всё?

Почти. Последнее, что хочу сказать, — это про лицензирование Vodka.
В прошлой статье я спрашивал вашего мнения о лицензии, и вы ответили — в основном «открывайте». Несмотря на это, мы решили, что код Vodka будет закрытым, но лицензий будет две — бесплатная и коммерческая для компаний.

Одну причину я уже называл в комментариях: загрузчик, формат, правка импортов и запуск драйверов, да ещё всё это с полным контролем — вместе это инструмент, которым можно сделать «доверенного клиента» там, где его не предполагалось, а это может сильно навредить. Выложить такое целиком — решение, которое потом не отменить. Если же мы перестанем его поддерживать и развивать, проект станет открытым.

Всем спасибо за прочтение! Будем рады ответить на ваши вопросы, жалобы и предложения в комментариях :)

Скрытый текст

P. S.

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


  1. strok_ova
    10.10.2026 14:38

    Спасибо за статью. Теперь всё стало намного понятнее

    Я правильно понимаю, что если запустился Quake, то и приложения Microsoft Office запускается?


    1. TizZy Автор
      10.10.2026 14:38

      Не совсем прямая связь, но и с MS Office дела идут хорошо!