Добавь газку: +200% производительности
Привет, Хабр. В прошлый раз я рассказывал тебе, как мы написали Raw конвертер на JavaScript, а ты сказал мне, что он работает медленно. Сегодня я хочу рассказать о том, как мы ускорили наш raw.pics.io почти в 3 раза. Я не буду постить простыни кода с описанием каждого шага, постараюсь рассказать в общем виде о подходах к оптимизации, которые мы использовали. Также я решил не писать о доступе к DOM, уменьшении количества HTTP-запросов, склеивании и минификации файлов, опциях сжатия на сервере и т.д. Все это техническая работа, о которой написали хорошо и много.Как все начиналосьМы начали с определения критических участков кода, которые занимают основную часть времени выполнения. Практически всегда в программах есть гадкие куски, которые тормозят больше всего — вот с них и стоит начать. Обычно разработчик примерно представляет, куда уходит основное время в его коде, и найти то, что тормозит, не составляет особого труда. Нужно только захотеть. Помимо сакральных знаний мы использовали профайлер и замеряли время выполнения с помощью console.time (). Кроме того, cейчас в браузерах для этих целей появился удобный объект performance.Оптимизация на уровне технологий Технологическая оптимизация, наверное, самый глобальный из всех подходов к увеличению производительности. Можно сменить язык, компилятор или стек ипользуемых технологий для того, чтобы добиться нужных результатов. В нашем случае самый значительный прирост производительности дал переход на параллельные вычисления.SIMD (Single Instructions Multiple Data) Используя MMX и SSE можно добиться существенного ускорения за счет быстрого исполнения операций над векторами. Такая штука уже есть во многих языках, а недавно Intel представила расширение языка, добавляющее такие возможности в JavaScript. Эта новость несказанно обрадовала нас. Мы поискали и даже нашли реализацию в ночной сборке Firefox. Но радость была преждевременной. После пары тестов мы поняли, что сейчас это невозможно использовать. Реализация SIMD, которую мы попробовали, на данный момент работает очень медленно. Мы продолжаем следить за развитием, но эта технология пока ещё достаточно сырая для использования.WebCL Ещё одна потенциально интересная технология, которую мы очень хотим использовать — это WebCL. Пару месяцев назад Khronos Group опубликовали первую спецификацию, описывающюю использование мощностей GPU для параллельных вычислений в браузере. Звучит здорово, но пока доступно только в виде синтетических тестов для специально собранных версий браузеров, или в виде плагинов. Надеюсь, удастся потестировать ближе к концу года. Пока неприменимо для использования вообще. =(WebWorkers В итоге задача дебайеризации отлично легла на WebWorkers, работать с которыми оказалось очень удобно и просто. Мы делим целое изображение на куски и обрабатываем каждый в своем потоке. Сперва мы написали свою обертку для управления WebWorker«ами, а сейчас используем библиотеку Parallel.js. Пришлось позаботится о том, чтобы нарезать, а потом склеивать эти куски. Нужно помнить о том, что на инициализацию каждого воркера уходит какое-то время и есть сложности с передачей некоторых типов данных в worker и обратно. Но основная проблема: вебворкеры не очень удобно дебажить. Cейчас такая возможность есть только в Chrome. Ещё один интересный вопрос: как определить оптимальное количество воркеров? Мы нашли корреляцию с количеством ядер в системе, но в браузерах до сих пор нет возможности определить их число.В общем, из всего найденного самыми рабочими оказались WebWorkers, которые теперь трудятся у нас в raw.pics.io.
Оптимизация на уровне алгоритмов Одним из важных подходов является подбор правильной архитектуры и быстрых алгоритмов. Мы обрабатываем массивы из 20 000 000+ элементов, а значит, ускорение обработки каждого из элементов уменьшает общее время выполнения. Естественно, ускорение или избавление от ненужных операций может сильно помочь. Вот почему мы много анализировали и выбирали подходящие алгоритмы интерполяции, переписывали их по несколько раз, заменяли математические операции на битовые сдвиги и удаляли лишние проверки и условия. Эти милисекунды на большом количестве итераций дали существенную экономию. Например, мы смогли увеличить скорость работы алгоритма благодаря тому, что не обрабатывали незначимые участки фотографии (неактивные участки сенсора камеры) и убрали лишние проверки, которые не влияли на результат.Оптимизация структур и типов данных Типизированные массивы Современные браузеры предоставляют возможность использовать быстрые типизированные массивы (typed arrays), которые дают неплохой прирост производительности по сравнению с обычными массивами. Это не всегда применимо, но для операций, которые манипулируют бинарными данными это настоящий глоток свежего воздуха. Сейчас все наши вычисления построены на типизированных массивах — без них невозможно было бы добиться той скорости, которая у нас есть сейчас.Простые структуры Самая первая версия нашего декодера содержала красивые структуры данных, с иерархией классов и кучей модулей. Хотя это и было правильно с точки зрения ООП, но тормозило при инициализации и доступе к объектам. После того, как мы взглянули на это со стороны и ещё раз обдумали структуру, я упростил всё до нескольких модулей. Такая денормализация уменьшила количество модулей и связей между ними, что немного усложнило понимание, но это было сделано осознанно (для целей производительности).Оптимизация на уровне языка У Nicholas Zakas есть отличная серия посвященная производительности JavaScript. Я не буду рассматривать всё, что мы делали, а опишу основное. Тормозящий код складывается (получается произведением =)) из стоимости выполнения операции и количества таких операций. И если мы не можем уменьшить количество итераций, то стоит уменьшить стоимость каждой операции. На каждом шаге у нас происходил вызов функции, а то и двух. Вызов функции достаточно дорогая операция и имеет смысл избегать их внутри больших циклов, так как это всегда сильно затратно. В JavaScript нет механизма (как в C++), который позволит сказать компилятору, что эту функцию нужно вставить inline, поэтому мы денормализовали код и избавились от этих вызовов. Стало менее читаемо, но более быстро. Благодаря такому подходу мы достаточно сильно увеличили производительность больших циклов.Также стоит понимать, что в ваших циклах является инвариантами и не меняется на каждой итерации. Эти инварианты нужно выносить за пределы цикла. Простой пример:
// обычный цикл
for (var i=0; i // такой цикл работает быстрее
var size = items.length;
for (var i=0; i Ещё одним хорошим примером могут служить так называемые LookUpTables (LUT), в которых мы кэшируем некоторые значения вместо того, чтобы пересчитывать их на каждом шаге цикла. Некоторые переменные (яркость пикселя в нашем случае) могут повторяться и нет смысла вычислять их на каждом шаге. Но применение LUT не всегда оправданно. Иногда (когда элементы сильно разнятся от итерации к итерации) суммарные расходы на построение этой таблицы больше, чем расходы на расчет этих элементов. Оптимизация на уровне платформы
Реализации JavaScript движков разнятся от браузера к браузеру и один и тот же код работает по-разному в Chrome и FF. Я не буду касаться этого, так как мы ещё не занимались такого рода оптимизациями, но уверен, что можно писать код более приспособленный к конкретному браузеру. Если у кого-то есть мысли на этот счет, пожалуйста, прокомментируйте.Оптимизация восприятия
Самое интересное, что можно сделать — показать пользователю результат как можно быстрее. Это тоже некая оптимизация, которая может «подарить» вам несколько секунд, пока пользователь грустит и смотрит на крутящийся индикатор выполнения. Мы можем показать какой-то промежуточный результат, маленькие кусочки карты или предварительно загруженное изображение низкого качества — нужно дать пользователю возможность начать усваивать информацию хоть в каком-то виде. Например, Pinterest до того, как прогрузит изображения, показывает прямоугольники залитые усредненным цветом конкретного изображения. За счет этого создается эффект, что он работает быстрее. У нас тоже самое можно сделать, показывая embedded JPEG, и заменяя его на результат обработки raw данных.Когда заканчивать
Каждый новый шаг оптимизации даёт всё меньший прирост производительности. И как только прилагаемые усилия достаточно велики (например, нужно переписать достаточно большую часть кода), а прирост производительности уже минимален (меньше 5%), то, скорее всего, стоит остановиться и посмотреть. Возможно уже хватит, или вы что-то делаете не так. Хотя если ваши операции выполняются действительно долго, дополнительные 5% могут сэкономить пользователям драгоценные секунды.Послесловие
Полезные инструменты
Зачастую стоит выбирать между скоростью выполнения и затратами на память. И хотя сейчас память уже достаточно дешевая, всё ещё бывают случаи когда ее не хватает. Проследить за тем, кто сколько памяти расходует помогает профайлер. Но и его иногда не хватает, чтобы понять что происходит. Google Chrome предоставляет отличные инструменты для разработчика. Chrome можно запустить с интересными флагами, что иногда очень кстати. Например так можно получить доступ к объекту memory и методу gc (), которые обычно недоступны. А так — перенаправить все ошибки прямо в терминал и найти много интересного. По адресу chrome://about доступен полный список встроенных в Chrome утилит, которые могут здорово помочь при отладке и разработке.Как тестировать варианты
Когда вы нашли один тормозящий кусочек и есть несколько вариантов оптимизации, нужно понять, какой из них будет работать быстрее. Вместо того, чтобы писать несколько вариантов, полезно прогнать маленькие синтетические тесты. Мы используем jsperf.com. Такие бенчмарки хорошо дают понять, какой из выбранных подходов будет наиболее оптимальным. А если поискать, то можно найти много готовых тестов и переписать/дописать их для своего случая.В общем, оптимизировать или нет (и главное как), нужно решать в каждом конкретном случае и это решение зависит от множества факторов, которые трудно структурировать. Комбинируя приведенные выше способы, нам удалось ускорить конвертацию RAW файлов в несколько раз, что по началу казалось практически невозможным. А это значит, что и у вас всё получится.