Pull to refresh

Удаление мёртвого кода для начинающих

Reading time 4 min
Views 1.2K
Original author: Rob Sayre
От переводчика: к сожалению, так и не дождался перевода следующего поста от разработчиков браузеров насчёт IE9 vs SunSpider, пришлось переводить самому. Зачем ещё один? В отличие от остальных (первый, второй), которые давали только информацию для размышления, этот даёт и практические знания — описание подводных камней, с которыми может столкнуться каждый разработчик, запуская свой код в новом IE9.

Dean Hachamovitch: Одним из изменений в нашем новом JavaScript движке, под кодовым названием Chakra, является уничтожение мертвого кода, с целью повышения производительности работы реальных сайтов. [1]


Вчера я отправил баг после лёгкого анализа алгоритма удаления мёртвого кода в IE9. После этого команда IE9 выпустила platform preview 7 и обновила свой блог про IE.

Удаление мёртвого кода является допустимой оптимизацией, но очень легко поверхностные реализации могут приводить к непредсказуемому поведению и стать источником ошибок.


Что касается хрупкости алгоритма реализованного в IE9, наше исследование показало, что только следующие операции могут быть оптимизированы: -, +, ++, <<, +=, -=, и if(>). Например, если вы в том тесте поменяете это сравнение:

if (TargetAngle > CurrAngle) {

на

if (TargetAngle <= CurrAngle) {

то IE9 уже не будет удалять этот код. Любопытно, что разработчики IE9 решили не удалять и такие ресурсоёмкие операции, как умножение и деление. Выбранное для оптимизации множество операций, похоже, совпадает с тем, которое используется в математических тестах в SunSpider.

Что касается продуманности, то уже найдено несколько проблем в реализации удаления мёртвого кода в IE9. К первой, которую я затрону, привлёк моё внимание один из наших разработчиков, Andreas Gal. Dean в блоге про IE привёл такой пример:

В следующем примере, код в цикле постоянно перезаписывает одну и ту же переменную (в CS это известно как мёртвое сохранение), поэтому этот код может быть упрощён до одного вызова.

function func(a, b) {
   var x;
   var i = 300;
   while (i--) {
      x = a + b; // dead store
   }
}


Я думаю, этим примером Dean пытается показать, что раз x постоянно перезаписывается, то движок JavaScript может выполнить всего одну итерацию цикла и сэкономить время, так как все вызовы дают одно и тоже значение для x.

Это может быть верно для языков со статической типизацией, таких как C#, но, к сожалению, это не верно для JavaScript.

Эта функция может быть оптимизирована, если она вызвана как

func(1,2)

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

func(1, { valueOf: function() { alert("Hi Dean!"); return 2; } });

Здесь мы передаём объект, как второй аргумент (b), и в нём объявляем метод valueOf, который будет вызван операцией a + b. Неожиданный поворот? Эта динамическая сторона языка очень сильно затрудняет оптимизацию JavaScript, и задача по добавлению традиционных для компиляторов оптимизаций, таких как удаление мёртвого кода или удаление мёртвого сохранения, становится чем угодно, но не тривиальной.

IE9 Platform Preview #7 обрабатывает этот пример из их блога неправильно (можно попробовать). Если функция вызывается с объектом, в котором есть своя реализация метода valueOf, то IE9 ошибочно не вызывает этот обработчик.

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

func(1, [1,2,3,4,5])

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

Object.prototype.valueOf = function() { alert("IE9 is fast!"); }

Здесь мы переопределяем обработчик valueOf для всех объектов, включая литералы, и поэтому сложение a + b вызовет нашу реализацию valueOf. И опять IE9 ошибочно пропускает вызов нашего обработчика, и определить этот случай ещё сложнее, чем предыдущий потому, что теперь на «оптимизацию» уже может повлиять код в других файлах.

Вторая проблема поверхностной реализации, которую я опишу, была замечена Chris Leary, другим разработчиком из команды Mozilla. Цикл в том математическом тесте, который IE9 удаляет как мёртвый код, работает с глобальной переменной «Angles». Мы обнаружили, что IE9 всё равно удаляет вызов cordicsincos(), даже когда определение переменной «Angles» было убрано из исходника. В этом случае, правильным поведением было бы выбросить исключение ReferenceError, так как «Angles» нигде не определено. Если есть желание, попробуйте эту демку, чтобы проверить ошибку. В добавок, вы обнаружите еще множество проблем с алгоритмом удаления мёртвого кода в IE9, если начнёте использовать новые фичи ES5 такие, как getters для переменной Angles или объекта window.

То, что время моего поста совпало с выходом IE9 PP7, чистая случайность, и я рад, что все браузеры всё быстрее и быстрее выполняют JavaScript. Но, как сказано, я не думаю, что можно считать реализацию удаления мёртвого кода в IE9 серьезной оптимизацией широкого действия. Похоже, что она ни на чём не тестировалась, кроме теста SunSpider.

Очевидно, я мог что-то пропустить, так что еще больше информации должно появиться в Twitter Q&A session от команды IE завтра утром.

От переводчика: в комментариях к оригиналу приводятся альтернативные и более дешевые решения указанных проблем вместо глобального анализа. Надеюсь, разработчики IE9 исправят указанные проблемы, а разработчики SunSpider добавят side effects в свои тесты. Спасибо XPilot за то, что дал ссылку на этот замечательный анализ.
Tags:
Hubs:
+41
Comments 27
Comments Comments 27

Articles