256 КБ кода, чтобы обнулить 64 КБ памяти — починили прямо в эмуляции

Компилятор развернул цикл инициализации стека в 65 536 отдельных инструкций записи байта. Команду эмулятора x86 это так возмутило, что они научили транслятор распознавать шаблон и тихо сворачивать его обратно в цикл.

Вкратце

Каждая инструкция записи байта занимала 4 байта. 65 536 штук — это 256 килобайт машинного кода ради инициализации 64 килобайт стека: соотношение кода к данным 4 к 1.

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

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

Когда эмулятор решает, что ваш код слишком позорный, чтобы его исполнять

Обычная работа бинарного транслятора состоит в точном воспроизведении: исполнить чужие инструкции ровно как написано, просто на другом процессоре. Свежий Old New Thing Рэймонда Чена описывает редкий момент, когда команда нарушила это правило, потому что код был слишком ужасен, чтобы его терпеть.

The Old New Thing
The Old New Thing

Арифметика, от которой передёргивает

Программе нужно было выделить на стеке около 64 КБ и обнулить их. Стандартный рецепт такой: проба стека, одно вычитание из указателя стека, затем плотный цикл инициализации. Этот компилятор вместо этого полностью развернул цикл в 65 536 отдельных инструкций записи байта. По 4 байта каждая — это 256 килобайт машинного кода ради инициализации 64 килобайт данных. Чен, который признаёт: «Я не знаю, к какому именно процессору относилась эта история», рассказывает, что команда эмулятора добавила логику распознавания: когда транслятор узнавал эту чудовищную функцию, он заменял последовательность на 256 КБ компактным циклом, который и должен был написать компилятор, сворачивая цикл обратно прямо при трансляции.

Инсайдеры подтверждают: это не единичный случай

Практика, когда слои совместимости тихо латают кривой софт, уходит глубоко, и человек, который это застал, написал об этом в обсуждении:

«Люди из Transmeta рассказывали мне, как их трансляторы были набиты оптимизациями под частные случаи, чтобы чинить ужасы, обнаруженные в самой Microsoft Windows.» — комментатор с 24 тыс. кармы, пересказывающий инженеров Transmeta

Второй инженер привёл пример того же класса бедствий: игра вызывала fread(data, 1, 65536, fptr) вместо fread(data, 65536, 1, fptr), превращая одно чтение в 65 000 однобайтовых и растягивая 15-секундную загрузку до минут, как только его слой подмены перехватывал вызовы ОС.

Может, уродливый код и был быстрым кодом

Самое полезное возражение поставило под сомнение саму идею «плохо». Развёртывание циклов и существует, чтобы убрать накладные расходы на ветвление и счётчик:

«На первый взгляд 256 КБ ради инициализации меньшего объёма кажется глупостью, но что, если так было быстрее?» — комментатор, бросающий вызов недостающему „зачем“ в статье

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

Получайте такие разборы каждый день

Главные истории Hacker News — на email, в Telegram или RSS.

Следующая новость · 13 из 23 Читать дальше
🌍Other

Как часы отсчитывают время 28 800 ударами в час — показано в 3D

Пружина размером с ноготь хранит 40 часов хода, а колесо качается туда-сюда восемь раз в секунду, чтобы выдавать эту энергию порциями. Бартош Цехановский даёт растащить на экране каждую деталь.

★ 626 · 💬 114 · ciechanow.ski · 3 мин