При написании высокопроизводительного кода JavaScript стандартных инструментов профилирования, предлагаемых Chrome и др., Не всегда достаточно. Кажется, что они предлагают только детализацию на уровне функций, и поиск нужной информации может занять довольно много времени.
В .NET класс StopWatch
дает мне именно то, что мне нужно: субмикросекундное разрешение произвольных фрагментов кода.
Для JavaScript performance.now()
был довольно хорошим способом измерения производительности, но в ответ на Spectre и Meltdown все основные браузеры снизили разрешение даже до миллисекунды.
Чтобы процитировать MDN на performance.now ():
The timestamp is not actually high-resolution. To mitigate security threats such as Spectre, browsers currently round the result to varying degrees. (Firefox started rounding to 2 milliseconds in Firefox 59.) Some browsers may also slightly randomize the timestamp. The precision may improve again in future releases; browser developers are still investigating these timing attacks and how best to mitigate them.
Мне нужно время с точностью до микросекунд. На момент написания, браузеры, похоже, не предлагали никаких параметров или флагов для отключения этих измерений безопасности. Возможно, я ошибаюсь в поиске терминов, но единственные статьи, с которыми я сталкиваюсь, - это объяснения проблем безопасности и способов их устранения.
Здесь меня не интересует аспект безопасности - я тестирую критически важные для производительности фрагменты кода JavaScript на моем собственном компьютере, и единственное, что меня волнует, - это получение максимально точных измерений с минимальными усилиями.
На ум приходят два варианта:
Например, мне пришлось бы посвятить старую версию FireFox тестированию, а новую версию Chrome - браузеру. Это непрактично, так как мне нужно тестировать во всех браузерах (а также, желательно, во всех браузерах). Кроме того, новые оптимизации не реализованы в старых браузерах, поэтому тесты могут оказаться бесполезными.
Я видел различные старые сообщения в блогах по этому поводу, но ни одно из них, похоже, не достигло той высокой точности, которая мне нужна (в конце концов, для этого раньше был performance.now()
).
Как мне получить эффективную версию до Spectre performance.now()
, не прибегая к более старым версиям браузеров, виртуальным машинам и тому подобному?
Существуют ли в JavaScript какие-либо методы кодирования или библиотеки, обеспечивающие точность до микросекунд?
Существуют ли какие-либо параметры или флаги для трех вышеупомянутых браузеров, отключающие эти меры безопасности?
В конечном итоге я ищу способ точно измерить относительную производительность разных частей кода по сравнению друг с другом, поэтому, если есть решение, которое дает мне тики, а не микросекунды, это тоже было бы приемлемо, если это точный и работает во всех браузерах.
Учитывая то, что вы описываете, звучит как довольно низкоуровневые тайминги, по-видимому, это не полностью зависит от работы в браузере (где доступ к DOM, выполнение ajax и т.д. будет на порядки медленнее, чем любой запущенный код) - не могли бы вы вместо этого работать в среда node.js? У вас там process.hrtime
.
@ Mike'Pomax'Kamermans Интервалы. Проще говоря, мне нужен «секундомер», который может сказать мне, сколько времени потребовалось для выполнения определенного фрагмента кода, и после попытки оптимизировать этот фрагмент кода сравнить два времени, чтобы увидеть, улучшила ли моя оптимизация производительность или нет.
Можете ли вы запустить код в цикле без того, чтобы оптимизатор JIT делал что-то, что разрушает ваш микробенчмарк?
@JamesThorpe Ну и да, и нет. Я хочу измерить производительность чистого JavaScript, поэтому он не зависит от работы в браузере, но мне нужно знать, как он выполняет в браузере. NodeJS предоставит мне только метрики для Chrome, предположительно
@PeterCordes Выполнение тысяч итераций в цикле - это то, как я сейчас делаю свои тесты, но JIT-оптимизация здесь дает некоторую неопределенность. Кроме того, само зацикливание делает довольно утомительным выполнение тестов, так как мне приходится рефакторировать и изолировать код и эффективно делать его другим, просто чтобы иметь возможность измерить. В идеале я просто хочу отбросить некоторые точки измерения в существующем коде без необходимости все перепрограммировать,
Я думаю, что лучше всего поискать где-нибудь ручку, которая повторно включает высокоточную синхронизацию в современных браузерах; по-видимому, они сделали возможным использование таких сценариев, как ваш. Не знаю как, но думаю, это возможно. Однако вам, возможно, придется создать свой собственный Firefox и Chromium из исходного кода с отключенной этой опцией. В основном вы заботитесь об использовании этого в своем собственном браузере на одной или нескольких машинах, потому что это используется для тестирования.
@PeterCordes Справочная точка. Но скажем, например, если бы у вас был бесконечный цикл, работающий в веб-воркере, просто подсчитывающий (и полностью используя ядро ЦП в процессе), и вы бы спросили его текущее количество в двух разных моментах, это потенциально все равно будет полезной информацией. . Я просто не знаю точно, как это сделать (и возможно ли это с самого начала - не уверен в накладных расходах postMessage и т. д.)
@PeterCordes Да, я как бы ищу ручку, просто говорю, что я был бы открыт для других вариантов, если бы они существовали. Создание firefox и хрома из исходников - на самом деле довольно умная идея, я разберусь с ней.
Интересно; этот взлом цикла, надеюсь, даст вам точность лучше, чем мс, но, вероятно, с множеством микросекунд накладных расходов. Наверное, недостаточно для надежных атак Spectre. Есть ли в JS общие атомарные переменные, которые вы можете читать из одного потока, в то время как другой атомарно их увеличивает? Или зацикленный поток должен получить сообщение и ответить на него? (Я действительно не знаю javascript, просто процессоры / низкоуровневые вещи; я здесь для тегов [benchmarking]
и [performance]
: P)
Это также немного похоже на оптимизацию неправильной вещи: синхронизация отдельных фрагментов кода, когда полный кодовый путь уже установлен, или менее 2 мс предполагает, что у вас уже есть код кода, в котором оптимизация будет -эффективно-нерелевантной. Чтобы оптимизировать эти фрагменты кода, вы действительно больше обращаете внимание на анализ сложности, а не на анализ времени выполнения, так что ввод увеличенной длины не увеличивается с 2 мс до 2 секунд до 2000 лет на два порядка. И если вы используете имеют для их оптимизации, у вас есть код, для которого подойдет Node.js.
@PeterCordes JavaScript не выполняет потоки, все концептуально происходит в цикле сообщений в одном потоке - в то время как у вас есть подобные сервис-воркеры (которые можно рассматривать как потоки), вы должны публиковать сообщения между ними - нет гарантия того, как быстро это сообщение будет видно
@ Mike'Pomax'Kamermans: 2 мс - это около 8 миллионов тактовых циклов ЦП на ЦП с тактовой частотой 4 ГГц. Это во много тысяч раз дольше, чем окно не по порядку (224 мопса) на Skylake, даже при выполнении кода с небольшим количеством инструкций за такт. Это также достаточно долго, чтобы вы, вероятно, могли с пользой провести микробенчмаркинг чего-то, что вы могли бы использовать как часть цикла, без того, чтобы что-то полностью искажалось из-за того, что оно настолько мало, что обычно складывается в часть более крупной операции. (т.е. мы не говорим здесь о ++ x vs. x ++ или о других бессмысленных вещах, не глядя на вывод JIT asm.)
@ Mike'Pomax'Kamermans Один из примеров того, что я хочу протестировать, - это конкретные горячие пути в парсере. Проверка того, что оператор switch со 100 вариантами может быть на несколько% быстрее, чем оператор switch с только 20 вариантами и 2-3 if-elses. Оптимизация сильно отличается в Chrome, FireFox и Edge. Иногда в Chrome его нельзя оптимизировать дальше, но в FireFox это возможно. Помимо этого низкоуровневого примера, я ищу также более высокоуровневые тесты, которые все еще находятся в пределах доли миллисекунды.
@FredKleuver: Да, как гуру asm, это звучит разумно, чтобы попытаться профилировать / микробенчмаркинг таким образом, за исключением того, что ветвь микробенчмаркинга, которая может ошибочно предсказывать, - это жесткий. Современные предикторы ветвления легко обучаются, и создать такую же непредсказуемость, как и в вашем реальном сценарии использования, сложно. (И простое включение его в цикл искажает это.) Может быть, немного на малой стороне, особенно если он превращается в таблицу переходов. (Эээ, если случаи не являются целыми числами, как они должны быть в C, а вместо этого совпадают строки или что-то в этом роде, это немного больше и даже лучше подходит для микростендинга.)
@PeterCordes Интересно. Имея это в виду, результаты также будут отличаться от процессора к процессору, и я могу только зайти так далеко. Всегда будут ошибки, и это вполне приемлемо. Несомненно, микросекундная и миллисекундная точность отсчета времени - вот слабый плод здесь, поэтому я сосредоточен именно на этом.
Для одного оператора switch
вам может потребоваться наносекундная точность, чтобы получить полезные результаты. Некоторая беспорядочная многопоточность, которую вы взламываете, не будет стоить хлопот по сравнению с реализацией JS, чтобы напрямую дать вам время высокого разрешения. Для браузеров с открытым исходным кодом это возможно определенно, это может потребовать немного работы. Относительная производительность одного и того же ассемблера x86 может быть разной на разном оборудовании, поэтому вам не нужен метод синхронизации, который также зависит от процессора, что делает невозможным определение того, что к чему. (Но остерегайтесь вариаций частоты процессора в режиме турбо / энергосбережения в любом случае)
@PeterCordes Спасибо за полезную информацию. Я сейчас смотрю, как собрать Chromium :) Если я как-то разберусь с этим для Chrome и FF, я отправлю это как ответ. Надеюсь, что кто-то еще придет тем временем с существующим решением, но это, вероятно, принятие желаемого за действительное, ха
Я бы поискал коммиты, которые внесли это изменение, и посмотрел, есть ли в нем возможность отключить его в настройках конфигурации Firefox, у которых нет пункта меню графического интерфейса пользователя, просто похороненного где-то в about: settings. Или на самом деле я бы просто сначала посмотрел эти настройки и, возможно, погуглил, как отключить ограничение точности. Вероятно, вы не первый, кто этого захочет.
Да, вы бы так сказали, но я повсюду искал в Google, чтобы отключить ограничение точности - полное отсутствие результатов - вот что заставило меня задать этот вопрос. Я думаю, что нашел фиксацию в хроме: github.com/chromium/chromium/commit/… - мне кажется, что это довольно жестко запрограммировано.
@FredKleuver: Да, нет никаких признаков того, что API времени обходит TimeClamper::ClampTimeResolution
. Но если есть, вы не обязательно увидите это в том патче, потому что до этого уже было ограничение в 5 мкс. Во всяком случае, в источнике выглядит легко отключить. Замена static constexpr double kResolutionSeconds = 0.0001
на 1.0
будет работать, или для снижения накладных расходов, сделав TimeClamper::ClampTimeResolution
бездействующим или исключив вызовы к нему. (И, кстати, floor(time_seconds / kResolutionSeconds)
был бы более эффективным, чем floor(time_seconds * (1.0/kResolutionSeconds))
, чтобы избежать разделения времени выполнения.
У вас действительно есть ботаник снайпер, да :) Спасибо большое, очень полезная информация! Я еще не смог собрать хром (ни один крупный продукт ОС не удалось собрать с первого раза в Windows), так что хороших новостей с моей стороны пока нет.
В Firefox есть параметр конфигурации privacy.reduceTimerPrecision
, который отключает защиту от Spectre. Вы можете переключить его на false, используя страницу Firefox about: config (введите about:config
в адресную строку).
Разобрался с подсказкой на MDN.
Я читал об этой опции в FireFox еще тогда, когда отправлял этот вопрос, но в то время он, похоже, еще не реализован. Хотя он сейчас там. Я должен отметить, что переключение, которое на самом деле не отключает полностью подавление Spectre, оно просто ограничивает его до 20 мкс, что и было смягчением в v59. Все еще намного лучше, чем ограничение по умолчанию на 1-2 мс, но не совсем те 5 мкс, которые были раньше.
Начиная с Firefox 79, вы можете использовать таймеры с высоким разрешением, если заставите ваш сервер отправлять два заголовка с вашей страницей:
Starting with Firefox 79, high resolution timers can be used if you cross-origin isolate your document using the
Cross-Origin-Opener-Policy
andCross-Origin-Embedder-Policy
headers:Cross-Origin-Opener-Policy: same-origin Cross-Origin-Embedder-Policy: require-corp
These headers ensure a top-level document does not share a browsing context group with cross-origin documents. COOP process-isolates your document and potential attackers can't access to your global object if they were opening it in a popup, preventing a set of cross-origin attacks dubbed XS-Leaks.
Ref: https://developer.mozilla.org/en-US/docs/Web/API/Performance/now
На данный момент это не упоминается на этой странице, но с помощью небольшого эксперимента я пришел к выводу, что точность таймера составляет 20 мкс с этими заголовками, что в 50 раз лучше, чем точность, которую вы получаете по умолчанию.
(() => {
const start = performance.now();
let diff;
while ((diff = performance.now() - start) === 0);
return diff;
})();
Это возвращает 0,02 или значение, очень близкое к 0,02 с этими заголовками, и 1 без них.
Чтобы дать хорошую рекомендацию: зачем вам этот точный тайминг? А вам нужны точные метки времени или точность интервалы?