Русский
Русский
English
Статистика
Реклама

Производительность

Перевод Мы создали Web приложение для определения лиц и масок для Google Chrome

15.03.2021 10:06:02 | Автор: admin

Введение

Основная цель - обнаружение лица и маски в браузере, не используя бэкенд на Python. Это простое приложение WebApp / SPA, которое содержит только JS-код и может отправлять некоторые данные на серверную часть для следующей обработки. Но начальное обнаружение лица и маски выполняется на стороне браузера и никакой реализации Python для этого не требуется.

На данный момент приложение работает только в браузере Chrome.

В следующей статье я опишу более технические подробности реализации на основе наших исследований.

Есть 2 подхода, как можно это реализовать в браузере:

Оба они поддерживают WASM, WebGL и CPU бэкенд. Но мы будем сравнивать только WASM и WebGL, так как производительность на CPU очень низкая и не может быть использована в конечной реализации.

Демо тут

TensorFlow.js

На официальном сайте TensorFlow.js предлагает несколько обученных и готовых к использованию моделей с готовой постобработкой. Для обнаружения лиц в реальном времени мы взяли BlazeFace модель, онлайн демо доступно тут.

Больше информации о BlazeFace можно найти тут.

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

WASM (размер изображения для определения лица: 160x120px; размер изображения для определения маски: 64x64px)

WebGL (размер изображения для определения лица: 160x120px; размер изображения для определения маски: 64x64px)

Результаты производительности: получение кадров

Мы можем получать кадры с помощью соответствующих функций HTML API. Но и этот процесс занимает время. Поэтому нам нужно понимать, сколько времени мы потратим на такие действия. Ниже приведены соответствующие временные метрики.

Наша цель - обнаружить лицо как можно быстрее, поэтому мы должны получать каждый кадр стрима и обрабатывать его. Для этой задачи мы можем использовать requestAnimationFrame, который вызывается каждые 16,6мс или каждый фрейм.

Метод grabFrame() из интерфейса ImageCapture позволяет нам взять текущий кадр из видео в MediaStreamTrack и вернуть Promise, который вернет нам изображение в формате ImageBitmap.

Одиночный режим - это максимальная производительность при получении кадра. Режим синхронизации - это то, как часто мы можем получать кадр с распознаванием лиц.

Результаты производительности: определения лица и маски

Цветовая схема: < 6 fps красный, 7-12 fps оранжевый, 13-18 fps желтый, 19+ fps зеленый.

Результаты:

Мы не учитываем временные метрики при запуске приложения. Очевидно, что во время запуска приложения и первых запусков модели оно будет потреблять больше ресурсов и времени. Когда приложение находится в "теплом" режиме, только тогда стоит получать показатели производительности. Теплый режим в нашем случае - это просто позволить приложению проработать 5-10 секунд, а затем получать показатели производительности.

Возможные неточности в собранных временных показателях - до 50мс.

Модель BlazeFace была разработана специально для мобильных устройств и помогает достичь хорошей производительности при использовании TFLite на платформах Android и IOS (~ 50-200 FPS).

Больше информации тут.

Набор данных для переобучения модели с нуля недоступен (исследовательская группа Google не поделилась им).

BlazeFace содержит 2 модели:

  • Фронтальную камеру: размер изображения 128 x 128px, быстрее, но с меньшей точностью.

  • Задняя камера: размер изображения 256 x 256px, медленнее, но с высокой точностью.

Размер изображения

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

Изображение, используемое BlazeFace для распознавания лиц, имеет размер 128 x 128px. Исходное изображение будет изменено до этого размера с учетом его пропорций. Изображение, которое используется для обнаружения маски, имеет размер 64 x 64px.

Мы выбрали минимальные разрешения для обоих изображений с учетом требований к производительности и результатам. Такие минимальные изображения показали наилучшие результаты на ПК и мобильных устройствах. Мы используем изображения размером 64 x 64px для обнаружения маски, потому что размера 32 x 32px недостаточно для обнаружения маски с достаточной точностью.

Как получить лучшие изображения для анализа приложением?

Используя TensorFlow.js у нас есть следующие опции для получения лучшего изображения, чтоб использовать в приложении:

  • BlazeFace позволяет настроить точность определения лица. Мы установили этот показатель с высоким значением (> 0,9), чтобы избежать неожиданных положительных результатов (например "призраки" в темном помещении или затылок человека определяло как лицо).

  • BlazeFace позволяет настроить максимальное количество лиц, которые возвращаются после анализа изображения. Можно указать для этого параметра значение 1, чтобы вернуть только 1 лицо; можно установить значение 2 и более и в этом случае при обнаружении более 1-го лица сообщать о том, что один человек / лицо может находиться перед камерой.

  • BlazeFace возвращает область лица и ориентиры (глаза, уши, нос, рот). Таким образом у нас есть все возможности для проверки этих результатов, чтобы убедиться, что обнаруженные лица подходят для последующей обработки.

Такие проверки могут быть следующими:

  1. Область лица должна находиться в калибровочной рамке в X%.

  2. Все ориентиры или их часть должны быть в калибровочной рамке.

  3. Ширина и высота области лица должны быть достаточными для дальнейшего анализа

  4. Проверки ширины / высоты области лица и ориентиров выполняются очень быстро на стороне клиента с помощью JS.

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

Размер моделей определения маски

Для определения маски мы использовали MobileNetV2 и MobileNetV3 с различными типами и мультипликаторами.

Мы предлагаем использовать легкие или сверхлегкие модели с TensorFlow.js в браузере (<3Mb). Основная причина в том, что WASM работает быстрее с такими моделями. Это указано в официальной документации, а также подтверждено нашими тестами производительности.

Дополнительные ресурсы

  • WASM JS бэкенд: ~60Kb

  • OpenCV.js: 1.6Mb

  • Наше SPA приложение (+TensorFlow.js): ~500Kb

  • BlazeFace модель: 466Kb

Для веб приложений время до взаимодействия (TTI) с ~3.5Mb JavaScript кода и бинарниками + JSON модели размером 1.5Mb to 6Mb будет >10 сек в холодном режиме; в прогретом режиме ожидается TTI - 4-5 сек.

Если использовать Web Worker (и OpenCV.js будет только в воркерах), это значительно уменьшит размер основного приложения до 800-900Kb. TTI будет 7-8 секунд в холодном режиме; в прогретом режиме <5 сек.

Возможные подходы к запуску моделей нейронных сетей в браузере

Однопоточная реализация

Это реализация по умолчанию для браузеров. Мы запускаем обе модели для обнаружения лиц и масок в одном потоке. Ключевым моментом является обеспечение хорошей производительности для обеих моделей для работы с любыми проблемами в одном потоке. Это текущий подход, который использовался для получения указанных выше показателей производительности.

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

Использование Web Workers для запуска моделей в разном контексте и распараллеливание в браузере

В основном потоке JS запускается модель BlazeFace для обнаружения лиц, а обнаружение маски выполняется в отдельном потоке через Web Worker. Благодаря такой реализации мы можем разделить обе запущенные модели и ввести параллельную обработку кадров в браузере. Это положительно повлияет на общее восприятие UX в приложении. Веб-воркеры будут загружать библиотеки TensorFlow.js и OpenCV.js, основной поток JS - только TensorFlow.js. Это означает, что основное приложение будет запускаться намного быстрее, и тем самым мы сможем значительно сократить время TTI в браузере. Обнаружение лиц будет запускаться чаще, это увеличит FPS процесса обнаружения лиц. В результате процесс обнаружения маски будет запускаться чаще, а также будет увеличиваться FPS этого процесса. Ожидаемые улучшения до ~ 20%. Это означает, что указанные в статье выше FPS и миллисекунды могут быть улучшены на это значение.

При таком подходе к запуску разных моделей в разных контекстах с помощью веб-воркеров мы можем запускать больше моделей нейронных сетей в браузере с хорошей производительностью. Основным фактором здесь будут аппаратные характеристики устройства, на котором это запускается.

Мы реализовали такой подход в приложении, и он работает. Но у нас есть некоторые технические проблемы с обратным вызовом postMessage, когда веб-воркер отправляет сообщение обратно в основной поток. По каким-то причинам он вводит дополнительную задержку (до 200мс на мобильных устройствах), которая убивает улучшение производительности, которого мы достигли с помощью распараллеливания (эта проблема актуальна только для чистого JS, после реализации на React.js эта проблема исчезла).

Результаты реализации с Web Workers

Наша идея состоит в том, чтобы использовать веб-воркеры для запуска каждой модели в отдельном воркере / потоке / контексте и добиться параллельной обработки моделей в браузере. Но мы заметили, что callback-функция в воркере вызывается с некоторыми задержками. Ниже вы можете найти соответствующие измерения и выводы о том, от каких факторов это зависит.

Тестовые параметры:mobileNetVersion=V3mobileNetVersionMultiplier = 0.75mobileNetVersionType = float16thumbnailSize=32pxbackend = wasm

В приложении мы запускаем BlazeFace в основном потоке и модель обнаружения маски в веб-воркере.

Измерения времени обработки Web Worker на разных устройствах:

Приведенные выше результаты демонстрируют, что по некоторым причинам время для отправки обратного вызова из Web Worker в основной поток зависит от модели, работающей или использующей метод TensorFlow browser.fromPixels в этом Web Worker. Если он запущен с моделью, обратный вызов в Mac OS отправляется ~ 27мс, если модель не запущена - 5мс. Эта разница в 22мс для Mac OS может привести к задержкам 100300мс на более слабых устройствах и повлияет на общую производительность приложения при использовании Web Worker. В настоящее время мы не понимаем, почему это происходит.

Как повысить точность и уменьшить количество ложных срабатываний?

Нам нужно иметь больше контекста, чтобы принять соответствующее решение, чтобы сообщить об обнаружении лица и отсутствии маски. Такой контекст - это ранее обработанные кадры и их состояние (лицо есть или нет, лицо есть или нет, маска есть или нет). Мы реализовали плавающий сегмент для управления таким дополнительным контекстом. Длина сегмента настраивается и зависит от текущего FPS, но в любом случае она не должна быть больше 200300мс, чтобы не было видимых задержек. Ниже приведена соответствующая схема, описывающая идею:

Заключение:

Чем мощнее устройство, тем лучше результаты производительности при измерении:

Для ПК мы могли получить следующие показатели:

  • Определение лица: > 30fps

  • Определение лица + определение маски: до 45fps

Для мобильных устройств мы могли получить следующие показатели:

  • Определение лица: от 2.5fps до 12-15fps в зависимости от мобильного устройства

  • Определение лица + определение маски: от 2 до 12fps в зависимости от мобильного устройства

  1. Обращаем внимание, что видео всегда в реальном времени и его производительность зависит от самого устройства, но всегда будет от 30 кадров в секунду.

  2. Для модели обнаружения маски в большинстве случаев лучшие результаты демонстрирует модель MobileNetV2 0.35, типы не влияют на метрики производительности.

  3. Размер модели обнаружения маски зависит от типов. Поскольку типы не влияют на показатели производительности, рекомендуется использовать модели uint16 или float16, чтобы иметь меньший размер модели в браузере и более быстрый TTI.

  4. Среда выполнения WASM показывает лучшие результаты по сравнению с WebGL для модели BlazeFace. Это соответствует официальной документации TensorFlow.js относительно производительности небольших моделей (<3Mb):

    Для большинства моделей серверная часть WebGL по-прежнему будет превосходить серверную часть WASM, однако WASM может быть быстрее для сверхлегких моделей (менее 3Mb и 60млн операций умножения и добавления). В этом случае преимущества распараллеливания GPU перевешиваются фиксированными накладными расходами на выполнение шейдеров WebGL.

  5. Время TTI всегда лучше для моделей WASM по сравнению с WebGL с той же конфигурацией моделей.

  6. Производительность среды выполнения и моделей TensorFlow.js можно повысить, используя расширение WASM для добавления инструкций SIMD, позволяющих векторизовать и выполнять несколько операций с плавающей точкой параллельно. Предварительные тесты показывают, что включение этих расширений обеспечивает ускорение в 23 раза по сравнению с WASM. Больше информации здесь. Это все еще экспериментальная функция, которая по умолчанию не поставляется со средой выполнения. Также после релиза он будет доступен во время выполнения по умолчанию или через дополнительные параметры конфигурации.

  7. На стороне клиента ожидаемый размер приложения составляет ~ 3,5Mb JS кода со всеми зависимостями, 466Kb модели BlazeFace, от 1,1Mb до 5,6Mb модели обнаружения маски. Ожидаемое время TTI для приложения составляет > 10 секунд для мобильных устройств в холодном режиме; в теплом режиме - ~ 5сек.

  8. При использовании веб-воркеров OpenCV.js может быть загружен только в веб-воркеры, это значительно снижает TTI для основного приложения.

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

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

  11. На наш взгляд, текущему решению достаточно даже 4-5 кадров в секунду, чтобы обеспечить хорошее восприятие пользователем UX. Но важно не показывать на экране область лица или ориентиры, а управлять всем на видео / экране:

    • Выделяя на экране, когда мы обнаружили человека.

    • Информирование о маске / отсутствии маски с помощью текстовых сообщений или другого выделения экрана.

  12. При таком взаимодействии пользователя задержки между реальным временем и нашими метаданными на экране будут составлять 200300мс. Такие значения будут рассматриваться пользователями системы как некритические задержки.

Подробнее..

Кэширование данных увеличивает скорость даже в неожиданных случаях

13.04.2021 16:13:10 | Автор: admin

Нас учат, что чтение данных из оперативной памяти ужасно долгая операция. Приводят аналогии с офисом и удалённым складом, заставляют писать cache-friendly код и внушают смертельный страх перед промахами кэша. Ещё нас учат, что процессоры отлично умеют считать числа, и часто быстрее вычислить результат дважды, чем сохранять его в памяти. Оказывается, это не всегда так.

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

Задача

Допустим, у нас есть матрица A порядка 2000x2000. Нужно посчитать обратную ей матрицу по простому модулю N. Другими словами, надо найти такую матрицу A-1, что AA-1 mod N = E.

Поскольку вычисления у нас происходят в поле по модулю, итерационные методы обращения нам не подойдут. Будем использовать старый добрый метод Гаусса.

Этот пост посвящён оптимизации метода Гаусса под данный конкретный случай. В реальном проекте вычисление обратной матрицы происходит в отдельном WebWorker, в данном примере обойдёмся главным потоком.

Вспомогательные функции

Для работы программы нам потребуется четыре вспомогательные функции. Первая вычисление (1 / x) mod N по расширенному алгоритму Евклида:

function invModGcdEx(x, domain){    if(x === 1)    {        return 1;    }    else    {        //В случае 0 или делителя нуля возвращается 0, означающий некий "некорректный результат"        if(x === 0 || domain % x === 0)        {            return 0;        }        else        {            //Расширенный алгоритм Евклида, вычисляющий такое число tCurr, что tCurr * x + rCurr * N = 1            //Другими словами, существует такое число rCurr, при котором tCurr * x mod N = 1            let tCurr = 0;            let rCurr = domain;            let tNext = 1;            let rNext = x;            while(rNext !== 0)            {                let quotR = Math.floor(rCurr / rNext);                let tPrev = tCurr;                let rPrev = rCurr;                tCurr = tNext;                rCurr = rNext;                tNext = Math.floor(tPrev - quotR * tCurr);                rNext = Math.floor(rPrev - quotR * rCurr);            }            tCurr = (tCurr + domain) % domain;            return tCurr;        }    }}

Вторая корректное целочисленное деление по модулю. Наивное вычисление c = a % b во всех языках программирования не будет давать математически верный результат, если a отрицательное число. Поэтому заведём функцию, которая будет делить правильно:

function wholeMod(x, domain){    return ((x % domain) + domain) % domain;}

Последние две функции относятся к операциям над строками матрицы. Первая вычитание из строки матрицы домноженной на число другой:

function mulSubRow(rowLeft, rowRight, mulValue, domain){    for(let i = 0; i < rowLeft.length; i++)    {        rowLeft[i] = wholeMod(rowLeft[i] - mulValue * rowRight[i], domain);    }}

Последняя нужная нам функция умножение строки матрицы на число:

function mulRow(row, mulValue, domain){    for(let i = 0; i < row.length; i++)    {        row[i] = (row[i] * mulValue) % domain;    }}

Обращение матрицы

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

function invertMatrix(matrix, domain){    let matrixSize = matrix.length;    //Инициализируем обратную матрицу единичной    let invMatrix = [];    for(let i = 0; i < matrixSize; i++)    {        let matrixRow = new Uint8Array(matrixSize);        matrixRow.fill(0);        matrixRow[i] = 1;        invMatrix.push(matrixRow);    }    //Прямой ход: приведение матрицы к ступенчатому виду    for(let i = 0; i < matrixSize; i++)    {        let thisRowFirst = matrix[i][i];        if(thisRowFirst === 0 || (thisRowFirst !== 1 && domain % thisRowFirst === 0)) //Первый элемент строки 0 или делитель нуля, меняем строку местами со следующей строкой, у которой первый элемент не 0        {            for(let j = i + 1; j < matrixSize; j++)            {                let otherRowFirst = matrix[j][i];                if(otherRowFirst !== 0 && (otherRowFirst === 1 || domain % otherRowFirst !== 0)) //Нашли строку с ненулевым первым элементом                {                    thisRowFirst = otherRowFirst;                                        let tmpMatrixRow = matrix[i];                    matrix[i]        = matrix[j];                    matrix[j]        = tmpMatrixRow;                    let tmpInvMatrixRow = invMatrix[i];                    invMatrix[i]        = invMatrix[j];                    invMatrix[j]        = tmpInvMatrixRow;                    break;                }            }        }        //Обнуляем первые элементы всех строк после первой, отнимая от них (otherRowFirst / thisRowFirst) * x mod N        let invThisRowFirst = invModGcdEx(thisRowFirst, domain);        for(let j = i + 1; j < matrixSize; j++)        {            let otherRowFirst = matrix[j][i];            let mulValue      = invThisRowFirst * otherRowFirst;            if(otherRowFirst !== 0 && (otherRowFirst === 1 || domain % otherRowFirst !== 0))            {                mulSubRow(matrix[j],    matrix[i],    mulValue, domain);                mulSubRow(invMatrix[j], invMatrix[i], mulValue, domain);            }        }    }    //Обратный ход - обнуление всех элементов выше главной диагонали    let matrixRank = matrixSize;    for(let i = matrixSize - 1; i >= 0; i--)    {        let thisRowLast    = matrix[i][i];        let invThisRowLast = invModGcdEx(thisRowLast, domain);        for(let j = i - 1; j >= 0; j--)        {            let otherRowLast = matrix[j][i];            let mulValue     = invThisRowLast * otherRowLast;            if(otherRowLast !== 0 && (otherRowLast === 1 || domain % otherRowLast !== 0))            {                mulSubRow(matrix[j],    matrix[i],    mulValue, domain);                mulSubRow(invMatrix[j], invMatrix[i], mulValue, domain);            }        }        if(thisRowLast !== 0 && domain % thisRowLast !== 0)        {            mulRow(matrix[i],    invThisRowLast, domain);            mulRow(invMatrix[i], invThisRowLast, domain);        }        if(matrix[i].every(val => val === 0))        {            matrixRank -= 1;        }    }    return {inverse: invMatrix, rank: matrixRank};}

Проверим скорость на матрице 500 x 500, заполненной случайными значениями из поля Z / 29. После 5 испытаний получаем среднее время выполнения в ~9.4с. Можем ли мы сделать лучше?

Первое, что мы можем заметить в поле Z / N не больше N обратных элементов. Чтобы избежать многократного вызова алгоритма Евклида, мы можем вычислить все обратные значения один раз и при надобности брать уже готовые. Изменим функцию соответствующим образом:

function invertMatrixCachedInverses(matrix, domain){    let matrixSize = matrix.length;    //Инициализируем обратную матрицу единичной    let invMatrix = [];    for(let i = 0; i < matrixSize; i++)    {        let matrixRow = new Uint8Array(matrixSize);        matrixRow.fill(0);        matrixRow[i] = 1;        invMatrix.push(matrixRow);    }    //Вычисляем все обратные элементы заранее    let domainInvs = [];    for(let d = 0; d < domain; d++)    {        domainInvs.push(invModGcdEx(d, domain));    }    //Прямой ход: приведение матрицы к ступенчатому виду    for(let i = 0; i < matrixSize; i++)    {        let thisRowFirst = matrix[i][i];        if(domainInvs[thisRowFirst] === 0) // <--- Первый элемент строки 0 или делитель нуля, меняем строку местами со следующей строкой, у которой первый элемент не 0        {            for(let j = i + 1; j < matrixSize; j++)            {                let otherRowFirst = matrix[j][i];                if(domainInvs[otherRowFirst] !== 0) // <--- Нашли строку с ненулевым первым элементом                {                    thisRowFirst = otherRowFirst;                                        let tmpMatrixRow = matrix[i];                    matrix[i]        = matrix[j];                    matrix[j]        = tmpMatrixRow;                    let tmpInvMatrixRow = invMatrix[i];                    invMatrix[i]        = invMatrix[j];                    invMatrix[j]        = tmpInvMatrixRow;                    break;                }            }        }        //Обнуляем первые элементы всех строк после первой, отнимая от них (otherRowFirst / thisRowFirst) * x mod N        let invThisRowFirst = domainInvs[thisRowFirst]; // <---        for(let j = i + 1; j < matrixSize; j++)        {            let otherRowFirst = matrix[j][i];            let mulValue      = invThisRowFirst * otherRowFirst;            if(domainInvs[otherRowFirst] !== 0) // <---            {                mulSubRow(matrix[j],    matrix[i],    mulValue, domain);                mulSubRow(invMatrix[j], invMatrix[i], mulValue, domain);            }        }    }    //Обратный ход - обнуление всех элементов выше главной диагонали    let matrixRank = matrixSize;    for(let i = matrixSize - 1; i >= 0; i--)    {        let thisRowLast    = matrix[i][i];        let invThisRowLast = domainInvs[thisRowLast]; // <---        for(let j = i - 1; j >= 0; j--)        {            let otherRowLast = matrix[j][i];            let mulValue     = invThisRowLast * otherRowLast;            if(domainInvs[otherRowLast] !== 0) // <---            {                mulSubRow(matrix[j],    matrix[i],    mulValue, domain);                mulSubRow(invMatrix[j], invMatrix[i], mulValue, domain);            }        }        if(domainInvs[thisRowLast] !== 0) // <---        {            mulRow(matrix[i],    invThisRowLast, domain);            mulRow(invMatrix[i], invThisRowLast, domain);        }        if(matrix[i].every(val => val === 0))        {            matrixRank -= 1;        }    }    return {inverse: invMatrix, rank: matrixRank};}

Замерим на тех же условиях и получаем результат в те же ~9.4с. Прироста нет, потому что даже при относительно долгом вычислении алгоритма Евклида он вычисляется всего один раз для каждой строки матрицы и особого вклада во время не приносит. Замерим производительность и посмотрим, что ещё можно улучшить.

72% времени занимает деление по модулю при сложении строк матрицы! Ну что тут сказать, деление по модулю, пусть и немного модифицированное для отрицательных чисел это элементарная операция и ускорять её некуда. Алгоритм поменять тоже не получится, из чего мы делаем вывод, что дальнейшее улучшение невозможно и статью можно закрывать.

...Или всё же возможно?

Если деление по модулю занимает столько времени, может, все возможные результаты тоже стоит сохранить в кэш? Даже если это не поможет, попытаться всё равно стоит при текущем времени выполнения функция неюзабельна.

Итак, используется wholeMod()только в функции mulSubRow():

rowLeft[i] = wholeMod(rowLeft[i] - mulValue * rowRight[i], domain);

Нам нужно для всех возможных значений x = a - b * c в поле Z / N сохранить результат выражения x mod N. Воспользоваться периодичностью мы не сможем, потому что тогда для вычисления индекса снова придётся использовать деление по модулю. В итоге при 0 <= a, b, c < N получаем N + (N - 1)^2 возможных значений. Много, но деваться некуда.

Из этих значений (N - 1)^2 значений меньше 0. Поскольку отрицательные индексы невозможны, при индексировании значением a - b * c к нему нужно прибавить (N - 1)^2. Тогда функция для сложения строк модифицируется:

function mulSubRowCached(rowLeft, rowRight, mulValue, wholeModCache, cacheIndexOffset){    for(let i = 0; i < rowLeft.length; i++)    {        rowLeft[i] = wholeModCache[rowLeft[i] - mulValue * rowRight[i] + cacheIndexOffset];    }}

Заметим, что эта функция накладывает ограничение на mulValue его значение не может быть больше domain и перед вызовом функции его тоже надо привести в наше поле Z / N. Кроме этого, обычное деление по модулю используется в функции mulRow().

Помимо wholeMod в вычитании строк матриц, используется . Кроме того, появилась вышеуказанная проблема с ограничением mulValue. Во всех этих случаях деление описывается формулой x = (a * b) mod N. Зная, что кэш хранит значения x = (c - a * b) mod N, мы можем вычислить (a * b) mod N, взяв значение кэша при c = 0 и вычтя его из N. Тогда функция для умножения строки на число модифицируется следующим образом:

function mulRowCached(row, mulValue, domain, wholeModCache, cacheIndexOffset){    for(let i = 0; i < row.length; i++)    {        row[i] = domain - wholeModCache[cacheIndexOffset - row[i] * mulValue];    }}

И получаем новое обращение матрицы:

function invertMatrix(matrix, domain){    let matrixSize = matrix.length;    //Инициализируем обратную матрицу единичной    let invMatrix = [];    for(let i = 0; i < matrixSize; i++)    {        let matrixRow = new Uint8Array(matrixSize);        matrixRow.fill(0);        matrixRow[i] = 1;        invMatrix.push(matrixRow);    }    //Вычисляем все обратные элементы заранее    let domainInvs = [];    for(let d = 0; d < domain; d++)    {        domainInvs.push(invModGcdEx(d, domain));    }    //Вычисляем кэш деления по модулю    const сacheIndexOffset = (domain - 1) * (domain - 1);    let wholeModCache = new Uint8Array((domain - 1) * (domain - 1) + domain);     for(let i = 0; i < wholeModCache.length; i++)    {        let divisor      = i - сacheIndexOffset;      //[-domainSizeCacheOffset, domainSize - 1]        wholeModCache[i] = wholeMod(divisor, domain); //Whole mod    }    //Прямой ход: приведение матрицы к ступенчатому виду    for(let i = 0; i < matrixSize; i++)    {        let thisRowFirst = matrix[i][i];        if(domainInvs[thisRowFirst] === 0) //Первый элемент строки 0 или делитель нуля, меняем строку местами со следующей строкой, у которой первый элемент не 0        {            for(let j = i + 1; j < matrixSize; j++)            {                let otherRowFirst = matrix[j][i];                if(domainInvs[thisRowFirst] !== 0) //Нашли строку с ненулевым первым элементом                {                    thisRowFirst = otherRowFirst;                                            //Меняем строки местами                    let tmpMatrixRow = matrix[i];                    matrix[i]        = matrix[j];                    matrix[j]        = tmpMatrixRow;                    let tmpInvMatrixRow = invMatrix[i];                    invMatrix[i]        = invMatrix[j];                    invMatrix[j]        = tmpInvMatrixRow;                    break;                }            }        }        //Обнуляем первые элементы всех строк после первой, отнимая от них (otherRowFirst / thisRowFirst) * x mod N        let invThisRowFirst = domainInvs[thisRowFirst]; // <---        for(let j = i + 1; j < matrixSize; j++)        {            let otherRowFirst = matrix[j][j];            if(domainInvs[otherRowFirst] !== 0)            {                let mulValue = domain - wholeModCache[сacheIndexOffset - otherRowFirst * invThisRowFirst]; // <---                mulSubRowCached(matrix[j],    matrix[i],    mulValue, wholeModCache, сacheIndexOffset); // <---                mulSubRowCached(invMatrix[j], invMatrix[i], mulValue, wholeModCache, сacheIndexOffset); // <---            }        }    }    //Обратный ход - обнуление всех элементов выше главной диагонали    let matrixRank = matrixSize;    for(let i = matrixSize - 1; i >= 0; i--)    {        let thisRowLast    = matrix[i][i];        let invThisRowLast = domainInvs[thisRowLast];        for(let j = i - 1; j >= 0; j--)        {            let otherRowLast = matrix[j][i];            if(domainInvs[otherRowLast] !== 0)            {                let mulValue = domain - wholeModCache[сacheIndexOffset - otherRowLast * invThisRowLast]; // <---                mulSubRowCached(matrix[j],    matrix[i],    mulValue, wholeModCache, сacheIndexOffset); // <---                mulSubRowCached(invMatrix[j], invMatrix[i], mulValue, wholeModCache, сacheIndexOffset); // <---            }        }        if(domainInvs[thisRowLast] !== 0)        {            mulRowCached(matrix[i],    invThisRowLast, domain, wholeModCache, сacheIndexOffset); // <---            mulRowCached(invMatrix[i], invThisRowLast, domain, wholeModCache, сacheIndexOffset); // <---        }        if(matrix[i].every(val => val === 0))        {            matrixRank -= 1;        }    }    return {inverse: invMatrix, rank: matrixRank};}

Замерим производительность. На той же матрице 500x500 по модулю 29 получаем время выполнения в ~5.4с.

Простите, что?

Нет, серьёзно, как это возможно? Кэшируем результат деления. Операции на два такта. В век супермедленной памяти и супербыстрых процессоров. Получаем прирост в 40%. Как?

Да, использование JavaScript создаёт определённый оверхед. Но JIT его нивелирует. Видимо, либо он нивелирует его недостаточно, либо не всё, чему нас учат про cache-friendly код правда.

И да, размер кэша растёт квадратично. Но если сравнить среднее время в полях по разному модулю, то прирост будет не сильно отличаться:

В реальном проекте, где был применён этот метод, матрицы не рандомные и прирост ещё заметнее.

Заключение

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

Полный код я выложил на Pastebin.

Подробнее..

Перевод Используем GPU для повышения производительности JavaScript

06.05.2021 14:05:14 | Автор: admin
image

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

Но думали ли вы об использовании мощи GPU для повышения производительности веб-приложений?

В этой статье я расскажу о библиотеке ускорения JavaScript под названием GPU.js, а также покажу вам, как повысить скорость сложных вычислений.

Что такое GPU.js и почему его стоит использовать?


Если вкратце, GPU.js это библиотека ускорения JavaScript, которую можно использовать для любых стандартных вычислений на GPU при работе с JavaScript. Она поддерживает браузеры, Node.js и TypeScript.

Кроме повышения производительности если и множество других причин, по которым я рекомендую использовать GPU.js:

  • В основе GPU.js лежит JavaScript, что позволяет использовать синтаксис JavaScript.
  • Библиотека берёт на себя задачу автоматической транспиляции JavaScript на язык шейдеров и их компиляции.
  • Если в устройстве отсутствует GPU, она может откатиться к обычному движку JavaScript. То есть вы ничего не потеряете, работая с GPU.js.
  • GPU.js можно использовать и для параллельных вычислений. Кроме того, можно асинхронно выполнять множественные вычисления одновременно и на CPU, и на GPU.

Учитывая всё вышесказанное, я не вижу никаких причин не пользоваться GPU.js. Давайте узнаем, как его освоить.



Как настроить GPU.js?


Установка GPU.js для ваших проектов похожа на установку любой другой библиотеки JavaScript.

Для проектов Node


npm install gpu.js --saveoryarn add gpu.jsimport { GPU } from ('gpu.js')--- or ---const { GPU } = require('gpu.js')--- or ---import { GPU } from 'gpu.js'; // Use this for TypeScriptconst gpu = new GPU();

Для браузеров


Скачайте GPU.js локально или воспользуйтесь его CDN.

<script src="dist/gpu-browser.min.js"></script>--- or ---<script   src="http://personeltest.ru/aways/unpkg.com/gpu.js@latest/dist/gpu- browser.min.js"></script><script   rc="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/gpu.js@latest/dist/gpu-browser.min.js"></script><script> const gpu = new GPU(); ...</script>

Примечание: если вы работаете в Linux, то нужно убедиться, что у вас установлены нужные файлы, при помощи команды: sudo apt install mesa-common-dev libxi-dev

Вот и всё, что нужно знать об установке и импорте GPU.js. Теперь можно использовать программирование GPU в своём приложении.

Кроме того, я крайне рекомендую разобраться в основных функциях и концепциях GPU.js. Итак, давайте начнём с основ GPU.js.

Создание функций


В GPU.js можно задавать выполняемые на GPU функции при помощи стандартного синтаксиса JavaScript.

const exampleKernel = gpu.createKernel(function() {    ...}, settings);

Показанный выше пример демонстрирует базовую структуру функции GPU.js. Я назвал функцию exampleKernel. Как видите, я использовал функцию createKernel, выполняющую вычисления при помощи GPU.

Также необходимо указать размер выводимых данных. В приведённом выше примере я использовал для задания размера параметр settings.

const settings = {    output: [100]};

Выходные данные функции ядра могут быть 1D, 2D или 3D, то есть можно использовать до трёх потоков. Доступ к этим потокам внутри ядра можно получить с помощью команды this.thread.

  • 1D: [length] value[this.thread.x]
  • 2D: [width, height] value[this.thread.y][this.thread.x]
  • 3D: [width, height, depth] value[this.thread.z][this.thread.y][this.thread.x]

Также созданную функцию можно вызывать как любую функцию JavaScript, по её имени: exampleKernel()

Поддерживаемые ядрами переменные


Число


Внутри функции GPU.js можно использовать любые integer или float.

const exampleKernel = gpu.createKernel(function() { const number1 = 10; const number2 = 0.10; return number1 + number2;}, settings);

Boolean


Булевы значения тоже поддерживаются в GPU.js, аналогично JavaScript.

const kernel = gpu.createKernel(function() {  const bool = true;  if (bool) {    return 1;  }else{    return 0;  }},settings);

Массивы


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

const exampleKernel = gpu.createKernel(function() { const array1 = [0.01, 1, 0.1, 10]; return array1;}, settings);

Функции


В GPU.js также допустимо использование приватных функций внутри функций ядер.

const exampleKernel = gpu.createKernel(function() {  function privateFunction() {    return [0.01, 1, 0.1, 10];  }  return privateFunction();}, settings);

Поддерживаемые типы вводимых данных


В дополнение к вышеуказанным типам переменных функциям ядер можно передавать множество других типов вводимых данных.

Числа


Функциям ядер можно передавать числа integer или float, аналогично объявлению переменных, см. пример ниже.

const exampleKernel = gpu.createKernel(function(x) { return x;}, settings);exampleKernel(25);

1D-, 2D- или 3D-массивы чисел


Ядрам GPU.js можно передавать типы массивов Array, Float32Array, Int16Array, Int8Array, Uint16Array, uInt8Array.

const exampleKernel = gpu.createKernel(function(x) { return x;}, settings);exampleKernel([1, 2, 3]);

Функции ядер также могут получать сжатые в одномерные (preflattened) 2D- и 3D-массивы. Такой подход сильно ускоряет загрузку, для этого нужно использовать опцию GPU.js input.

const { input } = require('gpu.js');const value = input(flattenedArray, [width, height, depth]);

HTML-изображения


По сравнению с традиционным JavaScript, передача в функции изображений является новой возможностью GPU.js. При помощи GPU.js можно передавать функции ядра одно или несколько HTML-изображений в виде массива.

//Single Imageconst kernel = gpu.createKernel(function(image) {    ...})  .setGraphical(true)  .setOutput([100, 100]);const image = document.createElement('img');image.src = 'image1.png';image.onload = () => {  kernel(image);    document.getElementsByTagName('body')[0].appendChild(kernel.canvas);};//Multiple Imagesconst kernel = gpu.createKernel(function(image) {    const pixel = image[this.thread.z][this.thread.y][this.thread.x];    this.color(pixel[0], pixel[1], pixel[2], pixel[3]);})  .setGraphical(true)  .setOutput([100, 100]);const image1 = document.createElement('img');image1.src = 'image1.png';image1.onload = onload;....//add another 2 images....const totalImages = 3;let loadedImages = 0;function onload() {  loadedImages++;  if (loadedImages === totalImages) {    kernel([image1, image2, image3]);     document.getElementsByTagName('body')[0].appendChild(kernel.canvas);  }};

Кроме вышеперечисленного, для экспериментов с GPU.js можно выполнять множество других интересных операций. Они описаны в документации библиотеки. Так как теперь вы знаете основы, давайте напишем функцию с использованием GPU.js и сравним её производительность.

Первая функция с использованием GPU.js


Скомбинировав всё вышеописанное, я написал небольшое angular-приложение для сравнения производительности вычислений на GPU и CPU на примере перемножения двух массивов из 1000 элементов.

Шаг 1 функция для генерации числовых массивов из 1000 элементов


Я сгенерирую 2D-массив с 1000 чисел для каждого элемента и использую их для вычислений на последующих этапах.

generateMatrices() { this.matrices = [[], []]; for (let y = 0; y < this.matrixSize; y++) {  this.matrices[0].push([])  this.matrices[1].push([])  for (let x = 0; x < this.matrixSize; x++) {   const value1 = parseInt((Math.random() * 10).toString())   const value2 = parseInt((Math.random() * 10).toString())   this.matrices[0][y].push(value1)   this.matrices[1][y].push(value2)  } }}

Шаг 2 -функция ядра


Это самое важное в данном приложении, поскольку все вычисления на GPU происходят внутри неё. Здесь мы видим функцию multiplyMatrix, получающую в качестве входных данных два массива чисел и размер матрицы. Функция перемножит два массива и вернёт общую сумму, а мы будем измерять время при помощи API производительности.

gpuMultiplyMatrix() {  const gpu = new GPU();  const multiplyMatrix = gpu.createKernel(function (a: number[][], b: number[][], matrixSize: number) {   let sum = 0;     for (let i = 0; i < matrixSize; i++) {    sum += a[this.thread.y][i] * b[i][this.thread.x];   }   return sum;  }).setOutput([this.matrixSize, this.matrixSize])  const startTime = performance.now();  const resultMatrix = multiplyMatrix(this.matrices[0],  this.matrices[1], this.matrixSize);    const endTime = performance.now();  this.gpuTime = (endTime - startTime) + " ms";    console.log("GPU TIME : "+ this.gpuTime);  this.gpuProduct = resultMatrix as number[][];}

Шаг 3 функция умножения на CPU


Это традиционная функция TypeScript для измерения времени вычисления для тех же массивов.

cpuMutiplyMatrix() {  const startTime = performance.now();  const a = this.matrices[0];  const b = this.matrices[1];  let productRow = Array.apply(null, new Array(this.matrixSize)).map(Number.prototype.valueOf, 0);  let product = new Array(this.matrixSize);    for (let p = 0; p < this.matrixSize; p++) {    product[p] = productRow.slice();  }    for (let i = 0; i < this.matrixSize; i++) {    for (let j = 0; j < this.matrixSize; j++) {      for (let k = 0; k < this.matrixSize; k++) {        product[i][j] += a[i][k] * b[k][j];      }    }  }  const endTime = performance.now();  this.cpuTime = (endTime  startTime) +  ms;  console.log(CPU TIME : + this.cpuTime);  this.cpuProduct = product;}

Полный демо-проект можно найти в моём аккаунте GitHub.

CPU против GPU сравнение производительности


Настало время проверить, справедлива ли вся эта шумиха вокруг GPU.js и вычислений на GPU. Так как в предыдущем разделе я создал Angular-приложение, я использовал его для измерения производительности.


CPU и GPU время выполнения

Как мы видим, программе на GPU потребовалось для вычислений всего 799 мс, а CPU потребовалось 7511 мс, почти в 10 раз дольше.

Я решил на этом не останавливаться и провёл те же тесты в течение ещё пары циклов, изменив размер массива.


CPU и GPU

Сначала я попробовал использовать массивы меньшего размера, и заметил, что CPU потребовалось меньше времени, чем GPU. Например, когда я снизил размер массива до 10 элементов, CPU потребовалось всего 0,14 мс, а GPU 108 мс.

Но с увеличением размера массивов возникала чёткая разница между временем, требуемым GPU и CPU. Как видно из показанного выше графика, GPU побеждает.

Вывод


Из моего эксперимента по использованию GPU.js можно сделать вывод, что он может значительно повышать производительность JavaScript-приложений.

Но стоит подходить с умом и использовать GPU только для сложных задач. В противном случае мы впустую потратим ресурсы, а в конечном итоге и снизим производительность приложений, что видно из представленного выше графика.



На правах рекламы


VDS для проектов и задач любых масштабов это про наши эпичные серверы! Новейшие технологии и оборудование, качественный сервис. Поспешите заказать!

Подписывайтесь на наш чат в Telegram.

Подробнее..

Перевод Sparkplug неоптимизирующий компилятор JavaScript в подробностях

09.06.2021 20:16:20 | Автор: admin

Создать компилятор JS с высокой производительностью означает сделать больше, чем разработать сильно оптимизированный компилятор, например TurboFan, особенно это касается коротких сессий, к примеру, загрузки сайта или инструментов командной строки, когда большая часть работы выполняется до того, как оптимизирующий компилятор получит хотя бы шанс на оптимизацию, не говоря уже о том, чтобы располагать временем на оптимизацию. Как решить эту проблему? К старту курса о Frontend-разработке делимся переводом статьи о Sparkplug свече зажигания под капотом Chrome 91.


Вот почему с 2016 года мы ушли от синтетических бенчмарков, таких как Octane, к измерению реальной производительности и почему старательно работали над производительностью JS вне оптимизирующих компиляторов. Для нас это означало работу над парсером, стримингом [этой поясняющей ссылки в оригинале нет], объектной моделью, конкурентностью, кешированием скомпилированного кода...

Впрочем, повернувшись лицом к улучшению производительности фактического, начального выполнения JS, мы столкнулись с ограничениями процесса оптимизации интерпретатора. Интерпретатор V8 сам по себе быстрый и сильно оптимизированный, но интерпретаторам как таковым свойственны накладные расходы, избавиться от которых мы не можем: например на декодирование байт-кода или диспетчеризацию неотъемлемые части функциональности интерпретатора.

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

Выход из положения Sparkplug: новый неоптимизирующий компилятор JavaScript, который мы выпустили вместе с V8 9.1, он работает между интерпретатором Ignition и компилятором TurboFan.

Новый процесс компиляцииНовый процесс компиляции

Быстрый компилятор

Sparkplug создан компилировать быстро. Очень быстро. Настолько, что мы всегда можем компилировать, когда захотим, повышая уровень кода SparkPlug намного агрессивнее кода TurboFan, [подробнее здесь, этой ссылки в оригинале нет].

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

Второй трюк в отличие от большинства компиляторов Sparkplug не генерирует промежуточное представление (IR). Вместо этого Sparkplug компилирует прямо в машинный код за один линейный проход по байт-коду и выдаёт соответствующий выполнению байт-кода машинный код. На самом деле компилятор это switch внутри цикла for, который занят диспетчеризацией, чтобы за каждой инструкцией закрепить генерирующие машинный код функции.

// The Sparkplug compiler (abridged).for (; !iterator.done(); iterator.Advance()) {  VisitSingleBytecode();}

Недочёт промежуточного представления (IR) заключается в том, что компилятор ограничен возможностями оптимизации. Поскольку промежуточной архитектурно независимой стадии нет, этот факт также означает, что всю реализацию нужно переносить на каждую поддерживаемую архитектуру отдельно. Но, оказывается, ни то, ни другое не является проблемой: портировать код довольно легко, вместе с тем Sparkplug не нужно выполнять тяжёлую оптимизацию.

Технически проход по байт-коду выполняется дважды. В первый раз для того, чтобы обнаружить циклы, во второй для генерации кода. Мы планируем в конце концов избавиться от первого прохода.

Совместимые с интерпретатором фреймы

Добавление нового компилятора в зрелую виртуальную машину JS пугающая задача. Кроме стандартного выполнения в V8 мы должны поддерживать отладчик, профилирование центрального процессора с обходом стека, а значит, трассировки стека для исключений, интеграцию в стратегию динамического повышения уровня функции, замену на стеке, чтобы оптимизировать код горячих циклов. Работы много.

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

Стековый фрейм с указателями стека и фреймаСтековый фрейм с указателями стека и фрейма

Сейчас около половины читателей закричит: "Диаграмма не имеет смысла, стек направлен в другую сторону!" Ничего страшного, я сделал кнопку: думаю, стек направлен вниз.

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

Стековые фреймы для нескольких вызововСтековые фреймы для нескольких вызовов

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

Это основной макет стека для всех типов функций; затем идёт соглашение о передаче аргументов и о том, как в своём фрейме функция хранит значения. В V8 мы имеем соглашение для фреймов JS, что аргументы до вызова функции (включая приёмник) добавляются на стек в обратном порядке, а также о том, что первые несколько слотов стека это вызываемая функция, контекст, с которым она вызывается, и количество передаваемых аргументов. Вот наш стандартный фрейм JS.

Это соглашение о вызове JS, общее для оптимизированных и интерпретируемых фреймов, именно оно позволяет нам, например, при профилировании кода на панели производительности отладчика проходить по стеку с минимальными накладными расходами.

В случае Ignition соглашение становится более явным. Ignition интерпретатор на основе регистров, это означает, что есть виртуальные регистры (не путайте их с машинными!), которые хранят текущее состояние интерпретатора. включая локальные переменные (объявления var, let, const) и временные значения. Эти регистры содержатся в стековом фрейме интерпретатора, вместе с указателем на выполняемый массив байт-кода и смещением текущего байт-кода в массиве.

Sparkplug намеренно создаёт и поддерживает соответствующий фрейму интерпретатора макет фрейма. Всякий раз, когда интерпретатор сохраняет значение регистра, SparkPlug также сохраняет его. Делает он это по нескольким причинам:

  1. Это упрощает компиляцию Sparkplug; новый компилятор может просто отражать поведение интерпретатора без необходимости сохранять какое-либо отображение из регистров интерпретатора в состояние Sparkplug.

  2. Поскольку компилятор байт-кода выполнил тяжёлую работу по распределению регистров, такой подход ускоряет компиляцию.

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

  4. Тривиальной становится и замена на стеке (OSR). Замена на стеке это когда выполняемая функция заменяется в процессе выполнения; сейчас это происходит, когда интерпретированная функция находится в горячем цикле (в это время она поднимается до оптимизированного кода этого цикла) и где оптимизированный код деоптимизируется (когда он опускается и продолжает выполнение функции в интерпретаторе), любая работающая в интерпретаторе логика замены на стеке будет работать и для Sparkplug. Даже лучше: мы можем взаимозаменять код интерпретатора и SparkPlug почти без накладных расходов на переход фреймов.

Мы немного изменили стековый фрейм интерпретатора: во время выполнения кода Sparkplug не поддерживается актуальная позиция смещения. Вместо этого мы храним двустороннее отображение из диапазона адресов кода Sparkplug к соответствующему смещению. Для декодирования такое сопоставление относительно просто, поскольку код Sparklpug получается линейным проходом через байт-код. Всякий раз, когда стековый фрейм хочет узнать "смещение байт-кода" для фрейма Sparkplug, мы смотрим на текущую выполняемую инструкцию в отображении и возвращаем связанное смещение байт-кода. Аналогично, когда Sparkplug нужно узнать OSR из интерпретатора, мы смотрим на байт-код в смещении и перемещаемся к соответствующей инструкции Sparkplug.

Вы можете заметить, что теперь у нас есть неиспользуемый слот фрейма, где должно быть смещение байт-кода; избавиться от него мы не можем, поскольку хотим сохранить оставшуюся часть стека неизменной. Мы перепрофилируем этот слот стека, чтобы вместо него кешировать "вектор обратной связи" для текущей выполняющейся функции; это вектор, хранящий данные о форме объекта, и он должен быть загружен для большинства операций. Всё, что нам нужно делать, соблюдать осторожность с OSR, чтобы гарантировать, что мы подставляем либо правильное смещение байт-кода, либо правильный вектор обратной связи для этого слота. В итоге стековый фрейм Sparkplug выглядит так:

Полагаемся на встроенный код

На самом деле Sparkplug генерирует очень мало собственного кода. У JS сложная семантика, так что для выполнения даже самых простых операций требуется много кода. Принудительная повторная генерация такого кода при каждой компиляции оказалась бы плохим решениям по нескольким причинам:

  1. Из-за огромного количества кода, который необходимо сгенерировать, она значительно увеличила бы время компиляции.

  2. Этот подход увеличил бы потребление памяти кодом Sparkplug.

  3. Пришлось бы переписывать кодогенерацию для большого количества функциональности JS, что, вероятно, означало бы и больше ошибок, и большую поверхность атаки.

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

Вы можете подумать: "Ну и какой тогда смысл во всём этом? Разве Sparkplug не выполняет ту же работу, что и интерпретатор?" и во многом будете правы. Во многих отношениях Sparkplug является "просто" сериализацией выполнения интерпретатора, вызывая те же встроенные двоичные сниппеты и поддерживая тот же стековый фрейм. Тем не менее оно того стоит, потому что Sparkplug удаляет (или, точнее, предварительно компилирует) те самые неустранимые накладные расходы интерпретатора, такие как декодирование операндов и диспетчеризация следующего байт-кода.

Оказалось, что интерпретация эффективнее множества оптимизаций уровня центрального процессора: статические операнды динамически читаются из памяти интерпретатором, вынуждая процессор делать предположения о том, какими могут быть значения. Диспетчеризация к следующей инструкции байт-кода для сохранения производительности требует успешного прогнозирования ветви выполнения, и, даже если предположения и прогнозы верны, по-прежнему нужно выполнять декодирование и диспетчеризацию кода, а также занимать драгоценное пространство различных буферов и кешей. ЦП сам по себе эффективный интерпретатор, хотя он применяется к машинному коду. С этой точки зрения Sparkplug транспилятор из байт-кода Ignition в байт-код центрального процессора, перемещающий выполнение в "эмуляторе" к "нативному" выполнению.

Производительность

Так как же Sparkplug работает на практике? Мы выполнили несколько бенчмарков Chrome на наших ботах для замера производительности со Sparkplug и без него. Спойлер: мы очень довольны.

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

Speedometer

Speedometer это тест, который пытается эмулировать реально работающий фреймворк, создавая веб-приложение для отслеживания списка задач с использованием нескольких популярных фреймворков и проводя стресс-тестирование производительности этого приложения добавлением и удалением задач. Мы обнаружили, что это отличное отражение поведения загрузки и взаимодействия в реальном мире, и мы неоднократно обнаруживали, что улучшения в спидометре отражаются на наших реальных показателях. Со Sparkplug оценка Speedometer улучшается на 510 %, в зависимости от бота.

Среднее улучшение показателей спидометра с помощью Sparkplug по нескольким ботам производительности. Полосы на диаграмме ошибок указывают на диапазон между квартилямиСреднее улучшение показателей спидометра с помощью Sparkplug по нескольким ботам производительности. Полосы на диаграмме ошибок указывают на диапазон между квартилями

Обзор бенчмарка

Speedometer отличный ориентир, но он не показывает всей картины. Также у нас есть набор бенчмарков просмотра веб-страниц записи набора реальных веб-сайтов, их мы можем воспроизводить, а также скрипт небольших взаимодействий, с их помощью мы можем получить более реалистичное представление о том, как различные метрики ведут себя в реальном мире.

На этих тестах мы решили посмотреть метрику V8 Main-Thread Thread, измеряющую общее проведённое в V8 время (включая компиляцию и выполнение), в основном потоке (то есть исключая стриминговый парсинг или фоновую оптимизацию). Это лучший способ увидеть, насколько оправдан Sparkplug, без учёта других источников шума бенчмарка.

Результаты различны и сильно зависят от машины и веб-сайта, но в целом выглядят прекрасно: видно улучшение около 515 %.

Медианное улучшение времени работы V8 в основном потоке на наших бенчмарках для просмотра с 10 повторениями. Полосы на диаграмме указывают на диапазон между квартилямиМедианное улучшение времени работы V8 в основном потоке на наших бенчмарках для просмотра с 10 повторениями. Полосы на диаграмме указывают на диапазон между квартилями

Таким образом, V8 имеет новый сверхбыстрый неоптимизирующий компилятор, повышающий производительность V8 в реальных бенчмарках на 515 %. Он уже доступен в V8 v9.1 (укажите опцию --sparkplug), и мы выпустим его вместе с Chrome 91.

Вот что важно заметить: выделившись в отдельную область разработки, фронтенд не ограничивается одним только JavaScript, например есть множество тонкостей в том, каким образом браузер работает с CSS. Вместе с тем оптимизации на уровне браузера не означают, что можно больше не писать аккуратный, компактный и элегантный код. Скорее они означают, что теперь разработчики будут свободнее чувствовать себя, когда захотят написать сложную или тяжёлую функциональность. Если фронтенд вам интересен, вы можете обратить внимание на наш курс Frontend-разработчик, где получите комплексную подготовку, чтобы в дальнейшем писать и поддерживать приложения различного масштаба и уровня сложности.

Узнайте, как прокачаться и в других специальностях или освоить их с нуля:

Другие профессии и курсы
Подробнее..

Перевод Как мы ускорили запуск приложения Dropbox для Android на 30

05.03.2021 14:13:31 | Автор: admin

Запуск приложения это первое впечатление наших пользователи после установки приложения. Это то, что происходит каждый раз. Простое и быстрое приносит пользователям гораздо больше радости, чем приложение, которое имеет массу функций, но требует вечности, чтобы запуститься. Команда Dropbox Android потратила время и силы на измерение, выявление и устранение проблем, влияющих на время запуска приложения. В итоге мы сократили время запуска приложения на 30 %, и вот история о том, как мы это сделали.


Страшный подъём

Исторически сложилось так, что в Dropbox мы отслеживали начало работы приложения, измеряя, сколько времени прошло с момента, когда пользователь нажал на иконку нашего приложения, и до момента, когда приложение полностью загрузилось и готово к работе с пользователем. Мы абстрагировали измерения инициализации приложения таким образом:

perfMonitor.startScenario(AppColdLaunchScenario.INSTANCE)// perform the work needed for launching the applicationperfMonitor.stopScenario(AppColdLaunchScenario.INSTANCE)

Как управляемая данными команда мы отслеживали и контролировали инициализацию приложения с помощью графиков, которые могли увидеть все инженеры. На графике ниже показаны измерения процентиля 90 при запуске приложения в период с конца марта по начало апреля 2020 года.

Как видно на графике, время запуска приложения, кажется, не так уж сильно меняется. Небольшие колебания при запуске приложений, которые варьируются в пределах пары миллисекунд, вполне ожидаемы в приложении, с которым на самых разных устройствах и ОС работают миллионы пользователей.

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

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

Больше цифр

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

  1. Выполнение миграции.

  2. Загрузка сервисов приложения.

  3. Загрузка первых пользователей.

Мы начали наше исследование с профилирования в Android Studio, чтобы измерить производительность наших тестовых телефонов. Проблема с профилированием производительности при таком подходе заключалась в том, что тестовые телефоны не давали статистически значимой выборки того, насколько хорошо на самом деле работает запуск приложения. Приложение Dropbox на Android имеет более 1 миллиарда установок на Google Play Store, охватывает несколько типов устройств, некоторые из них старые Nexus 5s, другие самые новые, лучшие устройства Google. Было бы глупо пытаться профилировать такое количество конфигураций. Поэтому мы решили измерить производительность разных этапов запуска приложения при помощи пошагового сценария в производственной среде.

Вот обновлённый код инициализации, где мы добавили логирование трёх упомянутых выше шагов:

perfMonitor.startScenario(AppColdLaunchScenario.INSTANCE)perfMonitor.startRecordStep(RunMigrationsStep.INSTANCE)// perform migrations step wrokperfMonitor.startRecordStep(LoadAppServicesStep.INSTANCE)// load application services stepperfMonitor.startRecordStep(LoadInitialUsers.INSTANCE)// perform initial user loading stepperfMonitor.stopScenario(AppColdLaunchScenario.INSTANCE)

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

Мы нашли виновников

На приведённом ниже графике показано общее время запуска приложения с января по октябрь 2020 года.

В мае мы начали измерять все основные этапы инициализации приложения. Эти измерения позволили нам выявить и устранить основных виновников замедления запуска приложения.

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

Firebase Performance Library

Чтобы измерять и отправлять метрики о производительности приложений, в Google Firebase включена Firebase Performance Library. Она предоставляет полезные функциональные возможности: показатели производительности отдельных методов, а также инфраструктуру для мониторинга и визуализации производительности различных частей приложения. К сожалению, библиотека производительности Firebase также имеет некоторые скрытые издержки. Среди них дорогостоящий процесс инициализации и, как следствие, значительное увеличение времени сборки. При отладке мы обнаружили, что инициализация Firebase Suite длилась в семь раз дольше, когда был включен инструмент Firebase Performance.

Чтобы устранить проблему с производительностью, мы решили удалить инструмент Firebase Performance из приложения Android Dropbox. Firebase Performance library замечательный инструмент, который позволяет профилировать детали производительности очень низкого уровня, такие как отдельные вызовы функций, но поскольку мы не использовали эти возможности библиотеки, то решили, что быстрый запуск приложения важнее данных о производительности отдельных методов, и поэтому удалили ссылку на эту библиотеку.

Миграции

Есть несколько внутренних миграций, выполняемых каждый раз при запуске приложения Dropbox. Они могут включать обновление флагов функций, миграцию баз данных и т. д. К сожалению, мы обнаружили, что некоторые из этих миграций запускались при каждом запуске приложения. Ранее мы не замечали их плохой производительности, потому что приложение быстро запускалось на устройствах разработки и тестирования. К сожалению, этот миграционный код особенно плохо работал на старых версиях ОС и старых устройствах, что способствовало увеличению времени запуска.

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

Загрузка пользователя

Мы храним метаданные контактов пользователей Dropbox на устройстве в виде больших двоичных объектов JSON это устаревшая часть приложения. В идеале эти большие двоичные объекты должны читаться и преобразовываться в объекты Java только единожды. К сожалению, код извлечения пользователей вызывался несколько раз из разных устаревших функций приложения, и каждый раз этот код выполнял дорогостоящий синтаксический анализ JSON, чтобы преобразовать кастомные объекты JSON в объекты Java. Хранение контактов пользователей в формате JSON как таковое было устаревшим решением в архитектуре и оно было частью легаси-монолита. Чтобы устранить эту проблему немедленно, мы добавили функциональность кэширования анализируемых пользовательских объектов во время инициализации. Мы продолжаем разбивать легаси-монолит, и более эффективным и современным решением для хранения контактов пользователей было бы использование объектов базы данных Room и преобразование этих объектов в бизнес-объекты.

Что делается сейчас?

В результате удаления ссылки на Firebase Performance и дорогостоящих шагов миграции, а также благодаря кэшированию пользовательских загрузок мы подняли производительность запуска приложений Dropbox Android на 30 %. Благодаря этой работе мы также собрали дашборды, которые помогут предотвратить деградацию времени запуска приложений в будущем.

Мы взяли на вооружение несколько методов, которые, мы надеемся, не позволят нам повторить ошибки с производительностью. Вот пара из них:

Мы смотрим, что происходит с производительностью, когда мы добавляем сторонние библиотеки.

Обнаружив то, насколько библиотека Firebase Performance замедлила запуск нашего приложения, мы ввели добавление сторонних библиотек как процесс. Сегодня, прежде чем библиотеку можно будет добавить и использовать в нашей кодовой базе, нам необходимо измерить время запуска, время сборки и размер APK-файла приложения.

Мы всё кэшируем.

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

Заключение

Больше вкладывая в убедительную аналитику и измерения производительности, теперь команда ещё больше ориентируется на данные , что помогает нам вкладываться в кодовую базу, например убирать устаревший код C++ гораздо более уверенно. Сегодня мы следим за несколькими дашбордами, которые дают нам представление о производительности наиболее важных частей наших приложений, и у нас есть процессы, которые обеспечивают быстроту приложения Dropbox, чтобы оно радовало пользователей.


Узнайте подробности, как получить Level Up по навыкам и зарплате или востребованную профессию с нуля, пройдя онлайн-курсы SkillFactory со скидкой 40% и промокодомHABR, который даст еще +10% скидки на обучение.

Другие профессии и курсы
Подробнее..

Современный фронтенд без ошибок и костылей. 8 полезных докладов конференции DUMP

20.04.2021 22:19:53 | Автор: admin

Привет, Хабр!

На связи IT-конференция DUMP и программный комитет секции Фронтенд: Полина Гуртовая (frontend-разработчик в Evil Martians) и Егор Ходырев (тимлид, full stack-разработчик в Кнопке)

Кто согласен, что современный фронтенд это сложно? Ради чего мы мучаемся с настройкой Webpack? Почему реализация SSR требует писать столько кода, и нужен ли он нам вообще такой ценой? Кто виноват и что мы, как разработчики, можем сделать?

В этом году вместе с нашими спикерами постараемся максимально чётко ответить на эти и сотни других вопросов в секции Frontend.

Со своими идеями и решениями выступят:







Алексей Охрименко из Яндекс.Музыки выступит с докладом ''Трасси... что?''

Отладка приложения занимает 99% нашего времени. Кто-то пользуется Chrome DevTools, кто-то обходится обычным console.log, кто-то использует профайлеры. Зачастую этих инструментов более чем достаточно. Но есть еще один, не особо известный и популярный в JavaScript мире.
Трассировка процесс пошагового выполнения программы. В режиме трассировки программист видит последовательность выполнения команд и значения переменных на данном шаге выполнения программы, что позволяет легче обнаруживать ошибки.

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







Григорий Петров из Evrone представит доклад ''Нужен ли нам N(e/u)xt.js?''

Современный фронтенд это сложно. Если легаси проекты ограничены, то для новых приложений, кроме настройки Webpack и Babel, у нас есть HMR, SSR, code splitting, routing, кеширование, stream rendering и это, не считая фронтенд фреймворка и бэкенда, CI/CD и деплоя.

HMR "ломается" на приложениях сложнее hello world, настройку SSR в интернетах хором называют "адски сложной", ну, а роутинг, в уважающей себя связке фронт+бэк, можно неправильно организовать десятью конкурирующими способами.

Вся эта сложность породила новое направление jamstack, и такие решения, как Next.js и Nuxt.js "opinionated фреймворки", где все настроено за нас.

Григорий использует в докладе эти два фреймворка, чтобы рассказать об основных сложностях современной фронтенд-разработки и то, как мы можем с ними бороться: готовыми шаблонами, собственным кодом или новыми архитектурами приложений. Сложности увидим с позиции "почему так получилось?"








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

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







Андрей Гончаров из Hazelcast и тема его доклада, которая звучит так: Lifting state up is killing your app.

Слышали ли вы про lifting state up? Может ли одна из двенадцати ключевых концепций в официальной документации React приводить к плохой производительности? В рамках доклада мы сделаем простейший grid на React. Поэтапно разберем возникающие проблемы производительности. Увидим, что иногда и O(1) - это недостаточно быстро. Будем профилировать и рефакторить до тех пор, пока приложение не станет работать быстрее, чем вы успеете сказать React.






Леонид Семенов из InvestEngine выступит с докладом про Е2Е тесты в браузеры. Когда Cypress, а когда не очень.

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

Ответов может быть много, но, кажется, что очевиден один автотесты!
Как внедрить автоматизированное сквозное тестирование?
О двух внедрениях, двух инструментах, одном путешествии, проблемах по пути и о самом пути вы тут и узнаете.






Роман Лысов из Semrush расскажет, ''Как создавать React компоненты, которыми будет приятно пользоваться''.

Проблема кастомизации компонентов и их использовании друг с другом всегда болезненна, и в компании Semrush решили эту проблему.
Доклад Романа основан на 4-х летнем опыте создания дизайн-системы для большого числа разработчиков. Он расскажет про практические приемы и паттерны для написания общих компонентов, которые помогут сделать ваш API компонентов более гибким, понятным и предсказуемым, что в итоге сэкономит многие человеко-часы на обслуживании в будущем.







Людмила Мжачих из Mail.Ru Group в докладе ''Как тестировать фронтенд без тестировщиков и спать спокойно'' объяснит, почему процесс разработки не может обойтись без багов и как сводить их к минимуму. Это становится возможным только когда разработка и тестирование начинают жить вместе.

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









Полина Гуртовая из Evil Martians выступит с докладом ''RTC и Франкенштейн', в котором расскажет об особенностях использования WebRTC для боевых задач, опишет проблемы, которые поджидают разработчиков, и покажет способы их преодолевать.

Услышим и обсудим опыт наших спикеров 14 мая в форматах онлайн и офлайн. Полная программа конференции DUMP и билеты на сайте.

А пока спикеры готовятся к выступлениям, посмотри ТОП-3 выступлений секции фронтендеров с нашей прошлой конференции >>>

1. Виталий Дмитриев и его "Реактивное программирование. Как мыслить реактивно, а не проактивно"

2. Александра Шинкевич поделилась болью разработчика в докладе "Как внедрить стандарты разработки, чтобы никто не пострадал"

3. Вадим Макеев и 15 лет опыта: от создания и экспорта графики до оптимизации и вставки в его выступлении "Делайте из слона муху"

Подробнее..

Работа с большими решениями .NET 5 в Visual Studio 2019 16.8

10.02.2021 10:11:22 | Автор: admin

С выпуском .NET 5 миграция решений из .NET Framework увеличилась. В частности, мы начали наблюдать перемещение очень крупных решений. Чтобы обеспечить максимальное удобство, мы работали над оптимизацией Visual Studio для обработки решений, содержащих большое количество проектов .NET 5 и .NET Core. Многие из этих оптимизаций были включены в версию 16.8, и в этом посте рассматриваются внесенные нами улучшения.

Запуск компилятора C# и VB вне процесса

Roslyn, компилятор C# и Visual Basic, парсит и анализирует все решение для поддержки служб, таких как IntelliSense, Go to Definition и диагностика ошибок. В результате Roslyn имеет тенденцию потреблять ресурсы, которые увеличиваются пропорционально размеру открытого решения, что может стать весьма значительным для больших решений. Команда Roslyn уже работала над минимизацией этого воздействия, активно кэшируя информацию на диске, которая не требуется немедленно, но даже при таком кэшировании нельзя избежать необходимости хранить данные в памяти.

Чтобы уменьшить влияние на более крупные решения Visual Studio, команда Roslyn приложила значительные усилия, чтобы вывести компилятор Roslyn из процесса Visual Studio в его собственный процесс. Запуск Roslyn в собственном процессе освобождает ресурсы в самой Visual Studio и дает компилятору Roslyn больше места для выполнения своей работы. Для больших решений это может сэкономить до трети памяти, потребляемой Visual Studio при открытии большого решения.

Оптимизация узла зависимостей

Каждый проект .NET 5 и .NET Core имеет узел в обозревателе решений с именем Dependencies, который отображает все, от чего зависит проект: другие проекты, сборки, пакеты NuGet и т.д. В дополнение к отображению непосредственных зависимостей проекта, узел также показывает транзитивные зависимости проекта, т.е. все, от чего зависит каждая зависимость, и так далее. Для проекта любого разумного размера этот список транзитивных зависимостей может стать довольно большим.

К сожалению, первоначальная реализация узла Dependencies не была очень эффективной в том смысле, что она хранила информацию о переходных зависимостях в памяти. В нем содержалось гораздо больше информации, чем было необходимо, и большая часть данных была избыточной. Мы переписали код, чтобы сохранить только ту информацию, которая абсолютно необходима, и начали использовать существующую информацию о зависимостях, которая уже хранится в NuGet. Эта перезапись сэкономила до 1015% памяти, потребляемой Visual Studio при открытии большого решения.

Уменьшение дублирования информации в MSBuild

После Roslyn одним из других основных потребителей ресурсов в процессе Visual Studio является MSBuild. Это связано с тем, что в качестве механизма сборки большая часть среды IDE основана на объектной модели MSBuild. Хотя мы сделали сами файлы проектов намного меньше в .NET 5 и .NET Core, существует значительное количество вспомогательных файлов проектов, которые импортируются в проекты через SDK. Оценка всех этих файлов необходима для понимания и построения проекта и может потреблять до трети памяти, нужной Visual Studio при открытии большого решения.

Хотя файлы проекта генерируют много данных, большая часть этих данных повторяется, и мы начали дедупликацию этих данных в памяти. Строки - один из наиболее распространенных побочных продуктов системы проектов, и они хранят такую информацию, как имена файлов, параметры и пути. В частности, пути могут быть довольно длинными и потреблять много памяти, если их слишком много или они дублируются слишком много раз. Обеспечено хранение только одной копии строки, что позволяет сэкономить до 510% памяти, потребляемой Visual Studio при открытии большого решения.

Уменьшение количества копий проекта, удерживаемых системой проектов

Одним из важных аспектов проектирования системы проектов .NET 5 и .NET Core является асинхронность. Часто системе проекта требуется выполнять работу в ответ на действие пользователя (например, добавление новой ссылки в проект). Вместо того, чтобы блокировать Visual Studio, пока она завершает свою работу, система проектов позволяет пользователю продолжать работу в среде IDE и выполнять работу в фоновом режиме.

Поскольку пользователь может продолжать вносить изменения в проект (например, добавлять еще одну ссылку), пока система проектов обрабатывает предыдущие изменения в фоновом режиме, система проекта должна сохранять моментальные снимки данных проекта, чтобы гарантировать, что последующее действие не конфликтует с более ранним. В результате система проектов может легко получить сразу несколько копий данных проекта в памяти. Если система проекта не заботится об управлении этими копиями, они могут храниться дольше, чем необходимо, или даже просочиться и сохраниться навсегда. Мы активно работали над сокращением количества копий, которые мы храним одновременно.

Улучшения загрузки большого решения

Проделав всю эту работу, мы значительно оптимизировали работу с большими решениями .NET 5 и .NET Core. Начиная с версии 16.8, во многих наших тестах мы наблюдали увеличение в 2,5 раза размера решения, которое мы можем открыть, прежде чем столкнуться с проблемами ресурсов. Мы также наблюдаем снижение до 25% сбоев, сообщаемых из-за исчерпания ресурсов.

Перечисленные выше улучшения - это только начало изменений, которые мы вносим для улучшения работы с большими решениями в Visual Studio. Производительность отдельных решений по-прежнему может варьироваться в зависимости от размера решения, типа проектов, загруженных расширений и т.д., И есть области, которые мы ищем для улучшения. Мы рекомендуем всем пользователям, у которых возникают проблемы с медленной загрузкой или сбоями при загрузке решений, обращаться к нам по адресу vssolutionload@microsoft.com, чтобы мы могли продолжать улучшать загрузку решений всех типов!

Подробнее..

Перевод Производительность главнее всего

04.04.2021 16:09:41 | Автор: admin
image


Как создать быстрое программное обеспечение?

Неверный способ


Если вы программист, вы, вероятно, знакомы с этой цитатой Кнута:

Преждевременная оптимизация корень всех зол.


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

image

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

image

Я считаю эту логику ошибочной. Если ваша программа все еще является прототипом и выполняет, например, 1% (20%, 50%, 90%) того, что она должна делать, и она уже работает медленно, то она будет еще более медленной после того, как вы ее закончите, разве нет? Если вы заставите ее делать больше, почему он должна стать быстрее?

Если кто-то говорит:

Мы создаем программы сначала правильными, а потом производительными. Мы оптимизируем их после того, как они будут реализованы.


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

И у меня с этим проблемы. Это более или менее равносильно тому, что финальная производительность остается на волю случая. ЕСЛИ вам удастся найти какое-то огромное узкое место в производительности и если его изменение не повлияет на архитектуру, вы МОЖЕТЕ получить некоторое ускорение, да. Но никто не может вам этого гарантировать. Это ставка. Вы либо получите некое ускорение, либо нет. По сути, вы принимаете любую производительность с небольшим шансом на небольшое улучшение. И вы назовете это хорошей инженерией?

Может показаться, что в истории полно программ, которые после выпуска стали работать быстрее. Всего несколько примеров всплывают в памяти: Chrome известен как пионер многих улучшений скорости JS. Компиляторы Kotlin и Rust получили много ускорений. VS Code / Atom в конечном итоге стали более быстрыми версиями своих оригинальных прототипов Electron. И я не говорю, что невозможно ускорить программы после выпуска. Я говорю, что эти улучшения случайны. Им просто повезло. Они могли никогда не случиться так легко, как раньше.

Верный способ


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

Во-первых, если вы начнете с быстрой программы, гораздо легче предотвратить снижение производительности, чем начать с медленной программы и надеяться, что вы найдете простой способ ее ускорить.

Затем, если вы действительно серьезно относитесь к результативности вашей окончательной программы, каждое решение должно приниматься с учетом производительности. Платформа, язык, архитектура, фреймворк, пользовательский интерфейс, бизнес-задачи, бизнес-модель. Можно ли их сделать быстрыми? Можем ли мы использовать язык X или фреймворк Y, можно ли их сделать быстрыми? Можем ли мы сделать эту функцию быстрой? Если нет, то чем ее заменить? Как сделать интерфейс быстрым? Как сделать так, чтобы он появлялся быстро? Подобные решения легко принять на раннем этапе, но невозможно изменить позже. Например, за последние годы скорость JavaScript впечатляла, но он все еще не может быть таким же быстрым, как C ++ или даже Java. Если бы вы поставили перед собой цель создать быстрый язык, в итоге вы бы создали другой язык!

Позвольте мне сформулировать это так:

Преждевременная оптимизация корень всех зол это корень всех зол.
Подробнее..

Перевод Либо быстро, либо неправильно

02.06.2021 18:06:54 | Автор: admin
image


В 2018 году я упражнялся на Advent of Code (здесь вы можете посмотреть стримы моих решений). Каждый день в декабре они публикуют небольшую проблему, и вы должны написать программу, которая её решит. Обычно это занимает от пары минут до пары часов и это довольно весело, я рекомендую вам попробовать. Когда задача выполнена, она всегда доступна, не только в декабре.

Я понял, что есть два типа решений: те, которые могут вычислить ответ за пару миллисекунд, и те, которые будут считать ответ несколько лет. Если у вас получился второй вариант, вы делаете что-то неправильно. Нет смысла ждать, хотя, технически, это тоже может быть правильным.

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

С другой стороны, если решение медленное, вы можете использовать любую вычислительную мощность, и ее все равно будет недостаточно. Это могло бы сократить время работы с трех лет (на ноутбуке) до одного года (на самом мощном компьютере, который я только могу собрать). Какая разница? Все равно слишком долго.

Теперь перейдем к программному обеспечению. Легко назвать решения Advent Of Code ошибочными, когда они медленные, поскольку мы знаем, что быстрое решение должно существовать. С реальными проблемами никто этого не гарантирует.

За исключением некоторых случаев.

Собственно, довольно часто.

На самом деле, я бы сказал, почти всегда.

Давайте посмотрим. У меня есть библиотека под названием Datascript. Это устойчивая структура данных/база данных и так уж вышло, что она реализована для двух платформ: JS и JVM. Более того, она фактически написана на Clojure, и большая часть ее кодовой базы используется обеими платформами. Это означает, что мы знаем, что обе версии всегда совершают одни и те же действия. Есть небольшой слой, который покрывает специфичные для платформы детали, такие как типы данных и стандартная библиотека, но остальное является общим. Дело не в том, что одна версия является оригинальной, а другая неэффективным портом. Они обе играют в одну игру.

Вы можете подумать, что если они ведут себя одинаково, их производительность должна быть одинаковой, не так ли? Было бы логично так думать.

Давайте посмотрим на фактическое время, необходимое для компиляции кодовой базы и выполнения полного набора интеграционных тестов. Мы говорим о кодовой базе, которая составляет чуть более 9000 LOC, из которых 4000 тестов:

Clojure 1.10 on JVM:

  • REPL boot time: 1.5 sec
  • Compile time: 6.5 sec
  • Tests time: 0.45 sec


ClojureScript 1.10.439 with advanced compilation:

  • Compile time: 78 sec
  • Tests time: 1 sec


ClojureScript 1.10.439 without Google Closure compilation:

  • Compile time: 24 sec
  • Tests time: 1.3 sec


Итак, о чем нам говорят эти числа? По сути, для обработки одного и того же кода вы можете потратить ~8 секунд, 24 секунды или 78 секунд. Выбор за вами. Кроме того, запустив ту же программу, вы можете получить результат за полсекунды, одну секунду или почти полторы секунды.

Но подожди, Tonsky, их нельзя сравнивать! Это две большие разницы! Они созданы, чтобы делать совершенно разные вещи! Один из них работает в браузере!

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

Что так долго делают компиляторы ClojureScript/Google Closure? Они зря тратят ваше время, вот что. Конечно, никто не виноват, но, в конце концов, все это решение просто неверно. Мы можем делать то же самое намного быстрее, у нас есть доказательства, у нас есть средства для этого, но так уж получилось, что это не так. Но мы могли бы. Если бы захотели. Эти огромные накладные расходы, которые вы платите, вы платите зря. Вы ничего не получаете от JS, кроме удвоения времени выполнения и астрономического времени сборки.

То же самое относится ко всем языкам с ужасно долгим временем сборки. Дело не в том, что они не могли бы работать быстрее. Они просто предпочитают не делать этого. Программа на C ++ или Rust слишком долго компилируется? Что ж, OCaml, вероятно, мог бы скомпилировать эквивалентную программу менее чем за секунду. И это по-прежнему будет быстро на уровне машины.

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

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

Представьте себе: из Москвы в Новосибирск, сердце Сибири, летит самолет, который преодолевает 2800 километров за 4 часа. Еще есть поезд, который преодолевает такое же расстояние за три дня. В поезде нет душа, плохая еда, кровати, на которых нельзя спать. А самолет это комфортабельный современный самолет. Что бы вы выбрали? Цена такая же. Единственная разница это ваш комфорт и ваше время.

image

Если мы примем это как метафору разработки программного обеспечения, вы удивитесь, что программисты с радостью выбирают поезд. Они даже до хрипоты спорят, что есть неопровержимые причины для выбора поезда. Нет, они не возражают, чтобы компилятор не торопился поработать. Хотя существуют более быстрые способы добраться до того же пункта назначения. Легко запутаться, споря о деталях, но помните: мы все оказываемся в одном месте, независимо от того, на каком языке мы говорим.

Браузеры? Та же история. HTML довольно неэффективный способ разместить пиксели на экране. Компьютер, который мог бы отображать миллионы полигонов в кадре, может с трудом прогружать веб-страницу. Как и в случае с решениями Advent of Code, это не зависит от мощности вашего компьютера. И даже высокооптимизированный веб-код, основанный на Canvas и WebAssembly (Figma), заставляет вентиляторы моего Macbook крутиться при полной тишине при запуске собственного Sketch.

image

*похлопывает по крышке* Этот ПК способен запустить Crysis 3 в 4K на 144fps.
Но может ли он запустить Atom?


Просто существуют пределы того, насколько далеко может зайти это неправильное решение. Текстовые редакторы на Electron не могут менять размер окна в реальном времени и проседают по кадрам, когда вы просто двигаете курсором. Slack на iMac Pro будет таким же медленным и требовательным к памяти, как и на 12-дюймовом Macbook.

Всё решение, web stack, в целом, ошибочно. То же самое можно было бы сделать быстрее и эффективнее ведь так много потраченного впустую потенциала. Пора признать это и начать все сначала. Существуют быстрые текстовые редакторы и программы для общения, которые вполне доступны даже для самых слабых нетбуков.

Я могу продолжать и продолжать. Следует помнить следующее: подумайте, что вы от этого получаете. Сопоставимы ли проблема и потраченные на нее ресурсы? Легко найти оправдания тому, почему дела обстоят так, как есть. Все они, вероятно, действительны, но это отговорки. Мы знаем, что возможны более быстрые программы, и это делает все остальное просто неправильным.
Подробнее..

Производительность компилятора при работе с концептами в C20

20.06.2021 18:15:44 | Автор: admin

Привет, меня зовут Александр, я старший разработчик ПО в Центре разработкиOrionInnovation. Хочу признаться, я люблю рассказывать про C++ и не только на различных митапах и конференциях.Ивотядобрался доХабра. НаCppConfRussiaPiter2020 я рассказывал про концепты и послевыступленияполучилочень много вопросов про производительность компилятора при работе сними.Замеры производительности не были цельюмоегодоклада:мне было известно, что концепты компилируются с примерно такой же скоростью, что и обычные метапрограммы,адодетального сравнения я смог добраться совершенно недавно.Спешуподелиться результатом!

Несколько слов о концептах

Концептыпереосмыслениеметапрограммирования, аналогичноеconstexpr.Еслиconstexprэто про вычисление выраженийво время компиляции, будь то факториал, экспонента и так далее, то концептыэто про перегрузки, специализации, условия существования сущностей.Вобщем, про чистоеметапрограммирование. Иными словами, в C++20 появилась возможность писать конструкциибез единой, привычной для нас треугольной скобки, тем самым получая возможность быстро и читаемо описать какую-либо перегрузку или специализацию:

// #1void increment(auto & arg) requires requires { ++arg; }; // #2void increment(auto &);struct Incrementable { Incrementable & operator++() { return *this; } };struct NonIncrementable {};void later() {    Incrementable     i;    NonIncrementable ni;    increment(i);  // Вызывается #1    increment(ni); // Вызывается #2}

О том, как всё это работает, есть море информации,например, отличный гайд "Концепты: упрощаем реализацию классов STD Utility" по мотивам выступления Андрея Давыдова на C++ Russia 2019. Ну а мы сфокусируемся на том, какой ценой достигается подобный функционал, чтобы убедиться, чтоэтонетолькопросто, быстро и красиво, ноещёи эффективно.

Описание эксперимента

Итак, мы будем наблюдать за следующими показателями:

  1. Время компиляции

  1. Размер объектного файла

  1. Количество символов в записи (или же количество кода), в некоторых случаях

Прежде чем мы начнём несколько важных уточнений:

  • Во-первых, при подсчёте количества символов в записи мы будем считать все не пустые.

  • Во-вторых, в данной статье мы посмотрим лишь на самые простые (буквально несколько строк) случаи, чтобы быть уверенными на 100%, что мы сравниваем абсолютно аналогичные фрагменты кода.

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

В замерах будут участвовать clang 12.0.0 и g++ 10.3.0, как с полной оптимизацией, так и без неё.

В качестве операционной системы выступит Ubuntu 16.04, запущенная на Windows 10 через WSL2.На всякий случай прилагаю характеристики ПК:

Характеристики ПК
------------------System Information------------------         Operating System: Windows 10 Enterprise 64-bit (10.0, Build 19043) (19041.vb_release.191206-1406)                 Language: Russian (Regional Setting: Russian)      System Manufacturer: Dell Inc.             System Model: Latitude 5491                     BIOS: 1.12.0 (type: UEFI)                Processor: Intel(R) Core(TM) i5-8300H CPU @ 2.30GHz (8 CPUs), ~2.3GHz                   Memory: 32768MB RAM      Available OS Memory: 32562MB RAM                Page File: 9995MB used, 27430MB available------------------------Disk & DVD/CD-ROM Drives------------------------      Drive: C: Free Space: 26.5 GBTotal Space: 243.0 GBFile System: NTFS      Model: SAMSUNG SSD PM871b M.2 2280 256GB

Эксперименты

Посленеобходимыхотступлениймы можем,наконец,начать эксперименты.

Эксперимент 1: Эволюция метапрограммирования

Для началапосмотрим на то, как компиляторы справляются с созданием перегрузки функции для инкрементируемых инеинкрементируемыхтипов данных аргумента. Компилируемый код для C++ 03, 17 и 20 представлены ниже. Один из показателей, а именнообъем кода, можно оценить уже сейчас: видно, что количество кода существенно сокращается по мере эволюции языка, уступая место читаемости и простоте.

Код
incrementable_03.cpp
// copied from boosttemplate<bool C, typename T = void>struct enable_if { typedef T type; };template<typename T>struct enable_if<false, T> {};namespace is_inc {    typedef char (&yes)[1]; typedef char (&no)[2];struct tag {};struct any { template &lt;class T&gt; any(T const&amp;); };tag operator++(any const &amp;);template&lt;typename T&gt;static yes test(T const &amp;);static no test(tag);template&lt;typename _T&gt; struct IsInc{    static _T &amp; type_value;    static const bool value = sizeof(yes) == sizeof(test(++type_value));};}template<typename T>struct IsInc : public is_inc::IsInc<T> {};template<class Ty>typename enable_if<IsInc<Ty>::value>::type increment(Ty &);template<class Ty>typename enable_if<!IsInc<Ty>::value>::type increment(Ty &);struct Incrementable { Incrementable & operator++() { return *this; } };struct NonIncrementable {};void later() {    Incrementable     i;    NonIncrementable ni;    increment(i);    increment(ni);}
incrementable_17.cpp
#include <type_traits>template<class, class = std::void_t<>>struct IsInc : std::false_type {};template<class T>struct IsInc<T, std::void_t<decltype( ++std::declval<T&>() )>>    : std::true_type{};template<class Ty>std::enable_if_t<IsInc<Ty>::value> increment(Ty &);template<class Ty>std::enable_if_t<!IsInc<Ty>::value> increment(Ty &);struct Incrementable { Incrementable & operator++() { return *this; } };struct NonIncrementable {};void later() {    Incrementable     i;    NonIncrementable ni;    increment(i);    increment(ni);}
incrementable_20.cpp
void increment(auto & arg) requires requires { ++arg; };void increment(auto &);struct Incrementable { Incrementable & operator++() { return *this; } };struct NonIncrementable {};void later() {    Incrementable     i;    NonIncrementable ni;    increment(i);    increment(ni);}

Давайте взглянем на результаты:

Файл

Компиляция

Время, мс

Размер объектного файла, байт

Количество символов, шт

incrementable_03.cpp

clang

O0

43,02

1304

782

incrementable_17.cpp

clang

O0

67,46

1320

472

incrementable_20.cpp

clang

O0

43,42

1304

230

incrementable_03.cpp

clang

O3

47,21

1296

782

incrementable_17.cpp

clang

O3

77,77

1304

472

incrementable_20.cpp

clang

O3

45,70

1288

230

incrementable_03.cpp

gcc

O0

19,89

1568

782

incrementable_17.cpp

gcc

O0

34,71

1568

472

incrementable_20.cpp

gcc

O0

17,62

1480

230

incrementable_03.cpp

gcc

O3

18,44

1552

782

incrementable_17.cpp

gcc

O3

38,94

1552

472

incrementable_20.cpp

gcc

O3

18,57

1464

230

Как уже отмечалось ранее,количество кода существенно уменьшается по мере развития языка: c 782 до 472 и затем до 230.Разницапочти в 3,5 раза, если сравнитьС++20 и С++03 (на самом деле даже больше,т.к.порядка150170символов во всех примерахтестирующий код). Размеры объектного файла также постепенно уменьшаются. Что же современем компиляции? Странно, новремя компиляции 03 и 20 примерно равно, а вот в С++17в два раза больше. Давайте взглянем на код наших примеров: помимо всего прочего, в глаза бросается#includeв случае C++17. Давайте реализуемdeclval,enable_ifиvoid_tи проверим:

incrementable_no_tt.cpp
template<bool C, typename T = void>struct enable_if { typedef T type; };template<typename T>struct enable_if<false, T> {};template<bool B, typename T = void>using enable_if_t = typename enable_if<B, T>::type;template<typename ...>using void_t = void;template<class T>T && declval() noexcept;template<class, class = void_t<>>struct IsInc {    constexpr static bool value = false;};template<class T>struct IsInc<T, void_t<decltype( ++declval<T&>() )>>{    constexpr static bool value = true;};template<class Ty>enable_if_t<IsInc<Ty>::value> increment(Ty &);template<class Ty>enable_if_t<!IsInc<Ty>::value> increment(Ty &);struct Incrementable { Incrementable & operator++() { return *this; } };struct NonIncrementable {};void later() {    Incrementable     i;    NonIncrementable ni;    increment(i);    increment(ni);}

И давайте обновим нашу таблицу:

Файл

Компиляция

Время, мс

Размер объектного файла, байт

Количество символов, шт

incrementable_03.cpp

clang

O0

43,02

1304

782

incrementable_17_no_tt.cpp

clang

O0

44,498

1320

714

incrementable_20.cpp

clang

O0

43,419

1304

230

incrementable_03.cpp

clang

O3

47,205

1296

782

incrementable_17_no_tt.cpp

clang

O3

47,327

1312

714

incrementable_20.cpp

clang

O3

45,704

1288

230

incrementable_03.cpp

gcc

O0

19,885

1568

782

incrementable_17_no_tt.cpp

gcc

O0

21,163

1584

714

incrementable_20.cpp

gcc

O0

17,619

1480

230

incrementable_03.cpp

gcc

O3

18,442

1552

782

incrementable_17_no_tt.cpp

gcc

O3

19,057

1568

714

incrementable_20.cpp

gcc

O3

18,566

1464

230

Время компиляции на 17 стандарте нормализовалось и стало практически равно времени компиляции 03 и 20, однако количество кода стало близко к самому тяжёлому, базовому варианту. Так что, если у вас есть под рукой C++20 и нужно написать какую-то простую мета-перегрузку,смело можно использовать концепты. Это читабельнее, компилируется примерно с такой же скоростью, а результат компиляции занимает меньше места.

Эксперимент 2: Ограничения для методов

Давайте взглянем на еще одну особенность: ограничение для функции или метода (в том числе и для конструкторов и деструкторов) на примере типаOptionalLike, имеющего деструктор по умолчанию в случае, если помещаемый объект тривиален, а иначедеструктор, выполняющийдеинициализациюкорректно. Код представлен ниже:

Код
optional_like_17.cpp
#include <type_traits>#include <string>template<typename T, typename = void>struct OptionalLike {    ~OptionalLike() {        /* Calls d-tor manually */    }};template<typename T>struct OptionalLike<T, std::enable_if_t<std::is_trivially_destructible<T>::value>>{    ~OptionalLike() = default;};void later() {    OptionalLike<int>         oli;    OptionalLike<std::string> ols;}
optional_like_20.cpp
#include <type_traits>#include <string>template<typename T>struct OptionalLike{    ~OptionalLike() {        /* Calls d-tor manually */    }    ~OptionalLike() requires (std::is_trivially_destructible<T>::value) = default;};void later() {    OptionalLike<int>         oli;    OptionalLike<std::string> ols;}

Давайте взглянем на результаты:

Файл

Компиляция

Время, мс

Размер объектного файла, байт

Количество символов, шт

optional_like_17.cpp

clang

O0

487,62

1424

319

optional_like_20.cpp

clang

O0

616,8

1816

253

optional_like_17.cpp

clang

O3

490,07

944

319

optional_like_20.cpp

clang

O3

627,64

1024

253

optional_like_17.cpp

gcc

O0

202,29

1968

319

optional_like_20.cpp

gcc

O0

505,82

1968

253

optional_like_17.cpp

gcc

O3

205,55

1200

319

optional_like_20.cpp

gcc

O3

524,54

1200

253

Мы видим, что новый вариант выглядит более читабельным и лаконичным (253 символа против 319 у классического), однако платим за это временем компиляции: оба компилятора как с оптимизацией, так и без показали худшее время компиляции в случае с концептами. GCC аж в 22,5 раза медленнее. При этом размер объектного файла уgccне изменяется вовсе, а в случаеclangбольше для концептов. Классический компромисс: либо меньше кода, но дольше компиляция, либо больше кода, но быстрее компиляция.

Эксперимент 3: Влияние использования концептов на время компиляции

Мы знаем, что накладывать ограничения на типможноиспользуя именованные наборы требований, они же концепты.Такжеможно указать требования непосредственно в момент объявления шаблонной сущности. Давайте посмотрим, есть ли разница с точки зрения компилятора. Компилировать будем следующие фрагменты:

Код
inline.cpp
template<typename T>void foo() requires (sizeof(T) >= 4) { }template<typename T>void foo() {}void later() {    foo<char>();    foo<int>();}
concept.cpp
template<typename T>concept IsBig = sizeof(T) >= 4;template<typename T>void foo() requires IsBig<T> { }template<typename T>void foo() {}void later() {    foo<char>();    foo<int>();}

Сразу взглянем на результаты:

Файл

Компиляция

Время, мс

Размер объектного файла, байт

inline.cpp

clang

O0

38,666

1736

concept.cpp

clang

O0

39,868

1736

concept.cpp

clang

O3

42,578

1040

inline.cpp

clang

O3

43,610

1040

inline.cpp

gcc

O0

14,598

1976

concept.cpp

gcc

O0

14,640

1976

concept.cpp

gcc

O3

14,872

1224

inline.cpp

gcc

O3

14,951

1224

Как мы можем заметить,размеры получившихся объектных файлов идентичны, а показатели времени компиляции практически совпадают. Так что при выборе концепт илиinline-требованиеможнонезадумываться о производительности компилятора.

Эксперимент 4: Варианты ограничения функции

Теперь посмотрим на варианты наложения ограничения на шаблонные параметры на примере функций. Ограничить функцию можно аж четырьмя способами:

  • Имя концепта вместоtypename

  • Requires clauseпослеtemplate<>

  • Имя концепта рядом сauto

  • Trailing requiresclause

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

Код
instead_of_typename.cpp
template<typename T>concept IsBig = sizeof(T) >= 4;template<IsBig T>void foo(T const &) { }template<typename T>void foo(T const &) {}void later() {    foo<char>('a');    foo<int>(1);}
after_template.cpp
template<typename T>concept IsBig = sizeof(T) >= 4;template<typename T>    requires IsBig<T>void foo(T const &) { }template<typename T>void foo(T const &) {}void later() {    foo<char>('a');    foo<int>(1);}
with_auto.cpp
template<typename T>concept IsBig = sizeof(T) >= 4;template<typename T>void foo(IsBig auto const &) { }template<typename T>void foo(auto const &) {}void later() {    foo<char>('a');    foo<int>(1);}
requires_clause.cpp
template<typename T>concept IsBig = sizeof(T) >= 4;template<typename T>void foo(T const &) requires IsBig<T> { }template<typename T>void foo(T const &) {}void later() {    foo<char>('a');    foo<int>(1);}

А вот и результаты:

Файл

Компиляция

Время, мс

Размер объектного файла, байт

function_with_auto.cpp

clang

O0

40,878

1760

function_after_template.cpp

clang

O0

41,947

1760

function_requires_clause.cpp

clang

O0

42,551

1760

function_instead_of_typename.cpp

clang

O0

46,893

1760

function_with_auto.cpp

clang

O3

43,928

1024

function_requires_clause.cpp

clang

O3

45,176

1032

function_after_template.cpp

clang

O3

45,275

1032

function_instead_of_typename.cpp

clang

O3

50,42

1032

function_requires_clause.cpp

gcc

O0

16,561

2008

function_with_auto.cpp

gcc

O0

16,692

2008

function_after_template.cpp

gcc

O0

17,032

2008

function_instead_of_typename.cpp

gcc

O0

17,802

2016

function_requires_clause.cpp

gcc

O3

16,233

1208

function_with_auto.cpp

gcc

O3

16,711

1208

function_after_template.cpp

gcc

O3

17,216

1208

function_instead_of_typename.cpp

gcc

O3

18,315

1216

Как мы видим, время компиляции отличается незначительно, однако мы можем заметить следующее:

  • Вариант с использованием имени концепта вместоtypenameоказался самым медленным во всех случаях.

  • Вариантыtrailing requiresclauseили использование концепта рядом сautoоказались самыми быстрыми.

  • Варианты, где присутствуетtemplate<>на510% медленнее остальных.

  • Размерыобъектных файлов изменяются незначительно, однако вариант с именем концепта вместоtypenameоказался самым объемным в случаеgcc, а вариант сautoоказался наименее объемным в случаеclang.

Эксперимент 5: Влияние сложности концепта на время компиляции

Последнее, что мырассмотрим в рамкахданной статьи, и, наверное, самое интересное: влияние сложности концепта на время компиляции. Давайте возьмём и скомпилируем следующие примеры, где сложность используемого концепта (количество проверок или условий) возрастает от первого к последнему.

Код
concept_complexity_1.cpp
template<typename T>concept ConceptA = sizeof(T) >= 1;template<typename T>concept TestedConcept = ConceptA<T>;void foo(TestedConcept auto const &) {}void foo(auto const &) {}void later() {    int i { 0 };    int * ip = &i;    foo(i);    foo(ip);}
concept_complexity_2.cpp
template<typename T>concept ConceptA = sizeof(T) >= 1;template<typename T>concept ConceptB =  requires(T i, int x) {    { i++     } noexcept -> ConceptA;    { ++i     } noexcept -> ConceptA;    { i--     } noexcept -> ConceptA;    { --i     } noexcept -> ConceptA;    { i + i   } noexcept -> ConceptA;    { i - i   } noexcept -> ConceptA;    { i += i  } noexcept -> ConceptA;    { i -= i  } noexcept -> ConceptA;    { i * i      } noexcept -> ConceptA;    { i / i      } noexcept -> ConceptA;    { i % i      } noexcept -> ConceptA;    { i *= i     } noexcept -> ConceptA;    { i /= i     } noexcept -> ConceptA;    { i %= i     } noexcept -> ConceptA;    { i |  i     } noexcept -> ConceptA;    { i &  i     } noexcept -> ConceptA;    { i |= i     } noexcept -> ConceptA;    { i &= i     } noexcept -> ConceptA;    { ~i          } noexcept -> ConceptA;    { i ^  i      } noexcept -> ConceptA;    { i << x      } noexcept -> ConceptA;    { i >> x      } noexcept -> ConceptA;    { i ^=  i      } noexcept -> ConceptA;    { i <<= x      } noexcept -> ConceptA;    { i >>= x      } noexcept -> ConceptA;};template<typename T>concept ConceptC =  requires(T i, int x) {    { i++     } noexcept -> ConceptB;    { ++i     } noexcept -> ConceptB;    { i--     } noexcept -> ConceptB;    { --i     } noexcept -> ConceptB;    { i + i   } noexcept -> ConceptB;    { i - i   } noexcept -> ConceptB;    { i += i  } noexcept -> ConceptB;    { i -= i  } noexcept -> ConceptB;    { i * i      } noexcept -> ConceptB;    { i / i      } noexcept -> ConceptB;    { i % i      } noexcept -> ConceptB;    { i *= i     } noexcept -> ConceptB;    { i /= i     } noexcept -> ConceptB;    { i %= i     } noexcept -> ConceptB;    { i |  i     } noexcept -> ConceptB;    { i &  i     } noexcept -> ConceptB;    { i |= i     } noexcept -> ConceptB;    { i &= i     } noexcept -> ConceptB;    { ~i          } noexcept -> ConceptB;    { i ^  i      } noexcept -> ConceptB;    { i << x      } noexcept -> ConceptB;    { i >> x      } noexcept -> ConceptB;    { i ^=  i      } noexcept -> ConceptB;    { i <<= x      } noexcept -> ConceptB;    { i >>= x      } noexcept -> ConceptB;};template<typename T>concept ConceptD =  requires(T i, int x) {    { i++     } noexcept -> ConceptC;    { ++i     } noexcept -> ConceptC;    { i--     } noexcept -> ConceptC;    { --i     } noexcept -> ConceptC;    { i + i   } noexcept -> ConceptC;    { i - i   } noexcept -> ConceptC;    { i += i  } noexcept -> ConceptC;    { i -= i  } noexcept -> ConceptC;    { i * i      } noexcept -> ConceptC;    { i / i      } noexcept -> ConceptC;    { i % i      } noexcept -> ConceptC;    { i *= i     } noexcept -> ConceptC;    { i /= i     } noexcept -> ConceptC;    { i %= i     } noexcept -> ConceptC;    { i |  i     } noexcept -> ConceptC;    { i &  i     } noexcept -> ConceptC;    { i |= i     } noexcept -> ConceptC;    { i &= i     } noexcept -> ConceptC;    { ~i          } noexcept -> ConceptC;    { i ^  i      } noexcept -> ConceptC;    { i << x      } noexcept -> ConceptC;    { i >> x      } noexcept -> ConceptC;    { i ^=  i      } noexcept -> ConceptC;    { i <<= x      } noexcept -> ConceptC;    { i >>= x      } noexcept -> ConceptC;};template<typename T>concept TestedConcept = ConceptA<T> && ConceptB<T> && ConceptC<T> && ConceptD<T>;void foo(TestedConcept auto const &) {}void foo(auto const &) {}void later() {    int i { 0 };    int * ip = &i;    foo(i);    foo(ip);}
concept_complexity_3.cpp
template<typename T>concept ConceptA = sizeof(T) >= 1;template<typename T>concept ConceptB =  requires(T i, int x) {    { i++     } noexcept -> ConceptA;    { ++i     } noexcept -> ConceptA;    { i--     } noexcept -> ConceptA;    { --i     } noexcept -> ConceptA;    { i + i   } noexcept -> ConceptA;    { i - i   } noexcept -> ConceptA;    { i += i  } noexcept -> ConceptA;    { i -= i  } noexcept -> ConceptA;    { i * i      } noexcept -> ConceptA;    { i / i      } noexcept -> ConceptA;    { i % i      } noexcept -> ConceptA;    { i *= i     } noexcept -> ConceptA;    { i /= i     } noexcept -> ConceptA;    { i %= i     } noexcept -> ConceptA;    { i |  i     } noexcept -> ConceptA;    { i &  i     } noexcept -> ConceptA;    { i |= i     } noexcept -> ConceptA;    { i &= i     } noexcept -> ConceptA;    { ~i          } noexcept -> ConceptA;    { i ^  i      } noexcept -> ConceptA;    { i << x      } noexcept -> ConceptA;    { i >> x      } noexcept -> ConceptA;    { i ^=  i      } noexcept -> ConceptA;    { i <<= x      } noexcept -> ConceptA;    { i >>= x      } noexcept -> ConceptA;};template<typename T>concept ConceptC =  requires(T i, int x) {    { i++     } noexcept -> ConceptB;    { ++i     } noexcept -> ConceptB;    { i--     } noexcept -> ConceptB;    { --i     } noexcept -> ConceptB;    { i + i   } noexcept -> ConceptB;    { i - i   } noexcept -> ConceptB;    { i += i  } noexcept -> ConceptB;    { i -= i  } noexcept -> ConceptB;    { i * i      } noexcept -> ConceptB;    { i / i      } noexcept -> ConceptB;    { i % i      } noexcept -> ConceptB;    { i *= i     } noexcept -> ConceptB;    { i /= i     } noexcept -> ConceptB;    { i %= i     } noexcept -> ConceptB;    { i |  i     } noexcept -> ConceptB;    { i &  i     } noexcept -> ConceptB;    { i |= i     } noexcept -> ConceptB;    { i &= i     } noexcept -> ConceptB;    { ~i          } noexcept -> ConceptB;    { i ^  i      } noexcept -> ConceptB;    { i << x      } noexcept -> ConceptB;    { i >> x      } noexcept -> ConceptB;    { i ^=  i      } noexcept -> ConceptB;    { i <<= x      } noexcept -> ConceptB;    { i >>= x      } noexcept -> ConceptB;};template<typename T>concept ConceptD =  requires(T i, int x) {    { i++     } noexcept -> ConceptC;    { ++i     } noexcept -> ConceptC;    { i--     } noexcept -> ConceptC;    { --i     } noexcept -> ConceptC;    { i + i   } noexcept -> ConceptC;    { i - i   } noexcept -> ConceptC;    { i += i  } noexcept -> ConceptC;    { i -= i  } noexcept -> ConceptC;    { i * i      } noexcept -> ConceptC;    { i / i      } noexcept -> ConceptC;    { i % i      } noexcept -> ConceptC;    { i *= i     } noexcept -> ConceptC;    { i /= i     } noexcept -> ConceptC;    { i %= i     } noexcept -> ConceptC;    { i |  i     } noexcept -> ConceptC;    { i &  i     } noexcept -> ConceptC;    { i |= i     } noexcept -> ConceptC;    { i &= i     } noexcept -> ConceptC;    { ~i          } noexcept -> ConceptC;    { i ^  i      } noexcept -> ConceptC;    { i << x      } noexcept -> ConceptC;    { i >> x      } noexcept -> ConceptC;    { i ^=  i      } noexcept -> ConceptC;    { i <<= x      } noexcept -> ConceptC;    { i >>= x      } noexcept -> ConceptC;};template<typename T>concept ConceptE =  requires(T i, int x) {    { i++     } noexcept -> ConceptD;    { ++i     } noexcept -> ConceptD;    { i--     } noexcept -> ConceptD;    { --i     } noexcept -> ConceptD;    { i + i   } noexcept -> ConceptD;    { i - i   } noexcept -> ConceptD;    { i += i  } noexcept -> ConceptD;    { i -= i  } noexcept -> ConceptD;    { i * i      } noexcept -> ConceptD;    { i / i      } noexcept -> ConceptD;    { i % i      } noexcept -> ConceptD;    { i *= i     } noexcept -> ConceptD;    { i /= i     } noexcept -> ConceptD;    { i %= i     } noexcept -> ConceptD;    { i |  i     } noexcept -> ConceptD;    { i &  i     } noexcept -> ConceptD;    { i |= i     } noexcept -> ConceptD;    { i &= i     } noexcept -> ConceptD;    { ~i          } noexcept -> ConceptD;    { i ^  i      } noexcept -> ConceptD;    { i << x      } noexcept -> ConceptD;    { i >> x      } noexcept -> ConceptD;    { i ^=  i      } noexcept -> ConceptD;    { i <<= x      } noexcept -> ConceptD;    { i >>= x      } noexcept -> ConceptD;};template<typename T>concept ConceptF =  requires(T i, int x) {    { i++     } noexcept -> ConceptE;    { ++i     } noexcept -> ConceptE;    { i--     } noexcept -> ConceptE;    { --i     } noexcept -> ConceptE;    { i + i   } noexcept -> ConceptE;    { i - i   } noexcept -> ConceptE;    { i += i  } noexcept -> ConceptE;    { i -= i  } noexcept -> ConceptE;    { i * i      } noexcept -> ConceptE;    { i / i      } noexcept -> ConceptE;    { i % i      } noexcept -> ConceptE;    { i *= i     } noexcept -> ConceptE;    { i /= i     } noexcept -> ConceptE;    { i %= i     } noexcept -> ConceptE;    { i |  i     } noexcept -> ConceptE;    { i &  i     } noexcept -> ConceptE;    { i |= i     } noexcept -> ConceptE;    { i &= i     } noexcept -> ConceptE;    { ~i          } noexcept -> ConceptE;    { i ^  i      } noexcept -> ConceptE;    { i << x      } noexcept -> ConceptE;    { i >> x      } noexcept -> ConceptE;    { i ^=  i      } noexcept -> ConceptE;    { i <<= x      } noexcept -> ConceptE;    { i >>= x      } noexcept -> ConceptE;};template<typename T>concept ConceptG =  requires(T i, int x) {    { i++     } noexcept -> ConceptF;    { ++i     } noexcept -> ConceptF;    { i--     } noexcept -> ConceptF;    { --i     } noexcept -> ConceptF;    { i + i   } noexcept -> ConceptF;    { i - i   } noexcept -> ConceptF;    { i += i  } noexcept -> ConceptF;    { i -= i  } noexcept -> ConceptF;    { i * i      } noexcept -> ConceptF;    { i / i      } noexcept -> ConceptF;    { i % i      } noexcept -> ConceptF;    { i *= i     } noexcept -> ConceptF;    { i /= i     } noexcept -> ConceptF;    { i %= i     } noexcept -> ConceptF;    { i |  i     } noexcept -> ConceptF;    { i &  i     } noexcept -> ConceptF;    { i |= i     } noexcept -> ConceptF;    { i &= i     } noexcept -> ConceptF;    { ~i          } noexcept -> ConceptF;    { i ^  i      } noexcept -> ConceptF;    { i << x      } noexcept -> ConceptF;    { i >> x      } noexcept -> ConceptF;    { i ^=  i      } noexcept -> ConceptF;    { i <<= x      } noexcept -> ConceptF;    { i >>= x      } noexcept -> ConceptF;};template<typename T>concept TestedConcept = ConceptA<T> && ConceptB<T> && ConceptC<T> && ConceptD<T> &&                                       ConceptE<T> && ConceptF<T> && ConceptG<T>;void foo(TestedConcept auto const &) {}void foo(auto const &) {}void later() {    int i { 0 };    int * ip = &i;    foo(i);    foo(ip);}

Давайте взглянем на результат:

Файл

Компиляция

Время, мс

Количество символов, шт

concept_complexity_1.cpp

clang

O0

37,441

201

concept_complexity_2.cpp

clang

O0

38,211

2244

concept_complexity_3.cpp

clang

O0

39,989

4287

concept_complexity_1.cpp

clang

O3

40,062

201

concept_complexity_2.cpp

clang

O3

40,659

2244

concept_complexity_3.cpp

clang

O3

43,314

4287

concept_complexity_1.cpp

gcc

O0

15,352

201

concept_complexity_2.cpp

gcc

O0

16,077

2244

concept_complexity_3.cpp

gcc

O0

18,091

4287

concept_complexity_1.cpp

gcc

O3

15,243

201

concept_complexity_2.cpp

gcc

O3

17,552

2244

concept_complexity_3.cpp

gcc

O3

18,51

4287

Чего и следовало ожидать, в общем случае существенное увеличение сложности концепта (обратите внимание, что концепты в примерах рекурсивные,и каждый последующий включает многократные отсылки к предыдущим) приводит к увеличению времени компиляциилишьна 515%.

Заключение

В результате вышеописанных экспериментов мы можем сделать следующие выводы:

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

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

  • Время компиляции прямо пропорционально сложности концептов/constraint'ов.

Post Scriptum

Во-первых, к статье прилагаюссылку на гитхаб, пройдя по которой вы можете найтискриптыдля запуска тестов,а такжеиспользуемыев статьефрагменты кода и повторить некоторые (а может и все) тесты локально.

Ну а во-вторых,мне быочень хотелосьувидеть, как ведут себя компиляторы с более сложными конструкциями.Если вы знаете/придумали подходящиепримеры, смелопишите о них в комментариях,ияс радостьюпроизведузамеры.

Подробнее..

Перевод Люди подозревают, что технологии отстой, потому что они на самом деле отстой

05.04.2021 16:10:13 | Автор: admin
image


Джей Ситтер в своей статье "Люди подозревают, что технологии отстой" пишет о людях, которые продолжают использовать технологии, несмотря на серьезные неприятности, такие как очень тусклый экран или постоянные всплывающие окна, и ничего не делают с этим. Он делает вывод:

Если бы мой экран был на 5% яркости или если бы я не мог использовать свой телефон, не нажимая Отмена каждые пять секунд, я бы тратил часы или дни на Google, пытаясь найти решение, если бы это было то, что мне нужно. То, что эти люди в основном просто мирились проблемами, означает, что для них эти проблемы не могли быть заметно хуже, чем сама технология в своей основе.


Эти примеры немного экстремальны, но важно помнить, что они реальны. Это не преувеличение. Это происходило.

В обсуждениях в Твиттере люди продолжают отвечать, что этим пользователям следует:

  • сделать что-нибудь с этим,
  • искать замену,
  • или просто не делать ничего.


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

Чтобы доказать свою точку зрения, я решил записывать каждое прерванное действие в течение одного дня. Вот полный список, который я написал вчера, 24 сентября 2020 года:

На iPhone:

  • iOS 14 разряжает аккумулятор телефона от 80% до 20% за ночь (нет активности, намного хуже, чем iOS 13).
  • YouTube.app случайным образом прокручивается вверх.
  • Instagram сбрасывает положение прокрутки после блокировки/разблокировки телефона.
  • Состояние гонки на клавиатуре в DuoLingo во время набора текста.
  • AirPods просто случайным образом повторно подключались во время использования.
  • Shortcuts.app перестал реагировать на прикосновения примерно на 30 сек.
  • Интересно, почему мои приложения не обновлены, обнаружил девять приложений, ожидающих нажатия кнопки вручную.
  • Курсор Workflowy скрыт за панелью инструментов Workflowy, ввод текста происходил за клавиатурой:

  • AirPods показывали уведомление о подключении, но звук воспроизводился из динамика.
  • Разблокировка паролем сработала только с третьего раза.
  • Виджет облачности исчез при переключении на другое приложение.
  • YouTube забыл видео, которое я только что смотрел после блокировки/разблокировки телефона.
  • YouTube забыл о разрешении, которое я выбрал для видео, продолжая сбрасывать меня до 360p на экране 750p.


В macOS:

  • Потерян 1 час в попытках подключить монитор 4k @ 120 Гц к MacBook Pro.
  • Автозаполнение даты в рабочем процессе предлагает мне даты в 2021 году вместо 2020 года.
  • В контекстном меню macOS Теги выделены меньшим шрифтом:

    image
  • Transmission неожиданно закрылось.
  • Magic Trackpad не подключался сразу после загрузки, отображалось окно Нет трекпада.
  • Hammerspoon не загружал профиль при загрузке.
  • Telegram застрял с одним счетчиком непрочитанных сообщений.
  • При подключении iPhone для зарядки требуется обновление программного обеспечения.


Интернет:

  • Перетаскивание изображения из Firefox не работает, пока я не открою его на отдельной вкладке.
  • В embed отключен полноэкранный режим YouTube.
  • Загрузился Slack, я начал набирать ответ, потом он перезагрузился.
  • Твиттер обрезал важные части моего изображения, поэтому мне пришлось вручную кешировать его.
  • TVTime не удалось отметить серию как просмотренную.


Apple TV:

  • Infuse потребовалось 10 минут, чтобы получить ~ 100 имен файлов из общего ресурса smb.


Все это произошло за один случайный день! И не особо загруженный. И я не включил в список то, что делал в тот день (только VirtualBox может составить такой список за 20 минут). И я профессиональный разработчик программного обеспечения, 20 лет ежедневно работаю за экраном, пытаясь приручить его. Я полагаю, что моя структура использования уже организована более тщательно, чем у обычного человека. Я также использую самые дорогие продукты Apple и остаюсь строго внутри экосистемы Apple.

Если бы я решил потратить время на то, чтобы сократить этот список, что я теоретически мог бы сделать? Обновить где-нибудь программное обеспечение, чтобы я мог заряжать свой iPhone без отображения всплывающего окна каждый раз? Купить новый MacBook, совместимый с монитором? Думаю, я могу что-то сделать с Hammerspoon, хотя я уже потратил на это два часа и не решил. Но я чувствую, что это все решаемо.

В любом случае, это все уменьшит список с 27 неприятностей до 24! Как минимум 24 неприятности в день, с которыми мне приходится жить. Это тот мир, в котором М ВСЕ живем сейчас. Добро пожаловать.
Подробнее..

Перевод Мы отрендерили миллион страниц, чтобы понять, из-за чего тормозит веб

30.12.2020 12:20:04 | Автор: admin
Мы отрендерили 1 миллион самых популярных страниц веба, фиксируя все мыслимые метрики производительности, записывая все ошибки и замечая все запрошенные URL. Похоже, таким образом мы создали первый в мире набор данных, связывающий производительность, ошибки и использование библиотек в сети. В этой статье мы проанализируем, что наши данные могут сообщить о создании высокопроизводительных веб-сайтов.


  • Посещён 1 миллион страниц
  • Записано по 65 метрик каждой страницы
  • Запрошен 21 миллион URL
  • Зафиксировано 383 тысячи ошибок
  • Сохранено 88 миллионов глобальных переменных

Можно ли превзойти наш анализ? Мы опубликовали наш набор данных на Kaggle, поэтому вы можете обработать данные самостоятельно.

Зачем рендерить миллион веб-страниц?


Сегодня распространено мнение о том, что веб почему-то стал более медленным и забагованным, чем 15 лет назад. Из-за постоянно растущей кучи JavaScript, фреймворков, веб-шрифтов и полифилов, мы съели все преимущества, которые даёт нам увеличение возможностей компьютеров, сетей и протоколов. По крайней мере, так утверждает молва. Мы хотели проверить, правда ли это на самом деле, а также найти общие факторы, которые становятся причиной торможения и поломок сайтов в 2020 году.

Общий план был простым: написать скрипт для веб-браузера, заставить его рендерить корневую страницу миллиона самых популярных доменов и зафиксировать все мыслимые метрики: время рендеринга, количество запросов, перерисовку, ошибки JavaScript, используемые библиотеки и т.п. Имея на руках все эти данные, мы могли бы начать задаваться вопросами о том, как один фактор корреллирует с другим. Какие факторы сильнее всего влияют на замедление рендеринга? Какие библиотеки увеличивают время до момента возможности взаимодействия со страницей (time-to-interactive)? Какие ошибки встречаются наиболее часто, и что их вызывает?

Для получения данных достаточно было написать немного кода, позволяющего Puppeteer управлять по скрипту браузером Chrome, запустить 200 инстансов EC2, отрендерить миллион веб-страниц за пару выходных дней и молиться о том, что мы действительно правильно поняли ценообразование AWS.

Общие значения



Протокол, используемый для корневого HTML-документа

HTTP 2 сейчас более распространён, чем HTTP 1.1, однако HTTP 3 по-прежнему встречается редко. (Примечание: мы считаем сайты, использующие протокол QUIC как использующие HTTP 3, даже если Chrome иногда говорит, что это HTTP 2 + QUIC.) Это данные для корневого документа, для связанных ресурсов значения выглядят немного иначе.


Протокол, используемый для связанных ресурсов

Для связанных ресурсов HTTP 3 используется почти в 100 раз чаще. Как такое может быть? Дело в том, что все сайты ссылаются на одно и то же:


Самые популярные URL ссылок

Есть несколько скриптов, связанных с большой частью веб-сайтов. И это ведь означает, что эти ресурсы будут в кэше, правильно? Увы, больше это не так: с момента выпуска Chrome 86 ресурсы, запрашиваемые с разных доменов, не имеют общего кэша. Firefox планирует реализовать такой же подход. Safari разделяет свой кэш уже многие годы.

Из-за чего тормозит веб: прогнозируем time-to-interactive


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


Корреляции метрик с dominteractive

По сути, каждая метрика позитивно коррелирует с dominteractive, за исключением переменной 01, обозначающей использование HTTP2 или более старшей версии. Многие из этих метрик также положительно коррелируют друг с другом. Нам нужен более сложный подход, чтобы выйти на отдельные факторы, влияющие на высокий показатель time-to-interactive.

Некоторые из метрик это тайминги, измеряемые в миллисекундах. Мы можем взглянуть на их диаграмму размаха (ящик с усами), чтобы понять, на что браузеры тратят своё время.


Диаграмма размаха метрик таймингов. Оранжевая линия это медиана, ящик ограничивает с 25-го по 75-й перцентиль.

Один из способов определения отдельных факторов, влияющих на высокий показатель time-to-interactive выполнение линейной регрессии, при которой мы прогнозируем dominteractive на основании других метрик. Это означает, что мы назначаем каждой метрике вес и моделируем время dominteractive страницы как взвешенную сумму других метрик, плюс некая константа. Алгоритм оптимизации расставляет веса так, чтобы минимизировать погрешность прогноза для всего набора данных. Величина весов, определённая регрессией, сообщит нам что-то о том, как каждая метрика влияет на медленность страницы.

Из регрессии мы исключим метрики таймингов. Если мы потратим 500 мс на установку соединения, то это добавит 500 мс к dominteractive, но это не особо интересный для нас вывод. По сути, метрики таймингов являются результатами. Мы хотим узнать, что становится их причиной.


Коэффициенты регрессии для метрик, прогнозирующей dominteractive

Числа в скобках это коэффициенты регрессии, выведенные алгоритмом оптимизации. Можно интерпретировать их как величины в миллисекундах. Хотя к точным значениям нужно относиться скептически (см. примечание ниже), интересно увидеть порядок, назначенный каждому аспекту. Например, модель прогнозирует замедление в 354 мс для каждого перенаправления (редиректа), необходимого для доставки основного документа. Когда основной HTML-документ передаётся по HTTP2 или более высокой версии, модель прогнозирует снижение time-to-interactive на 477 мс. Для каждого запроса, причиной которого стал документ, она прогнозирует добавление 16 мс.

В процессе интерпретации коэффициентов регрессии нужно помнить о том, что мы работаем с упрощённой моделью реальности. На самом деле time-to-interactive не определяется взвешенной суммой этих входящих метрик. Очевидно, что есть причинные факторы, которые модель выявить не может. Бесспорной проблемой являются искажающие факторы. Например, если загрузка основного документа по HTTP2 коррелирует с загрузкой других запросов по HTTP2, то модель встроит это преимущество в веса main_doc_is_http2_or_greater, даже если ускорение вызвано запросами не к основному документу. Нам нужно быть аккуратными, сопоставляя показания модели с выводами о реальном положении дел.

Как на dominteractive влияет версия протокола HTTP?


Вот любопытный график dominteractive, разделённый по версиям протокола HTTP, используемым для доставки корневой HTML-страницы.


Диаграмма размаха dominteractive, разделённая по версиям протокола HTTP первого запроса. Оранжевая линия медиана, ящик ограничивает с 25-го по 75-й перцентиль. Проценты в скобках доля запросов, выполненная по этому протоколу.

Существует крошечная доля сайтов, до сих пор доставляемых по HTTP 0.9 и 1.0. И эти сайты оказываются быстрыми. Похоже, мы не можем отвязаться от того факта, что протоколы стали быстрее, а поэтому довольные программисты пользуются этим ускорением, доставляя в браузер больше данных.

Это показатели для версии протокола, используемой для доставки корневой HTML-страницы. А если мы рассмотрим влияние протокола, используемого для ресурсов, на которые ссылается этот документ? Если провести регрессию для количества запросов по версии протокола, то получится следующее.


Коэффициенты прогнозирующей dominteractive регрессии для количества запросов по версии протокола

Если бы мы поверили этим показателям, то пришли бы к выводу, что перемещение запрашиваемых ресурсов при переходе с HTTP 1.1 на 2 ускоряется 1,8 раза, а при переходе с HTTP 2 на 3 замедляется в 0,6 раза. Действительно ли протокол HTTP 3 более медленный? Нет: наиболее вероятное объяснение заключается в том, что HTTP 3 встречается реже, а немногочисленные ресурсы, передаваемые по HTTP 3 (например, Google Analytics), больше среднего влияют на dominteractive.

Как на dominteractive влияет тип контента?


Давайте спрогнозируем time-to-interactive в зависимости от количества переданных данных при разделении по типам передаваемых данных.


Коэффициенты прогнозирующей dominteractive регрессии для килобайтов, переданных инициатором запроса

Вот похожая регрессия, на этот раз здесь рассматривается количество запросов по типу инициатора запроса.


Коэффициенты прогнозирующей dominteractive регрессии для количества запросов, переданных инициатором запроса

Здесь запросы разделены по инициаторам запросов. Очевидно, что не все запросы равны. Запросы, вызванные элементом компоновки (например, CSS или файлов favicon), и запросы, вызванные CSS (например, шрифтов и других CSS), а также скриптами и iframe значительно замедляют работу. Выполнение запросов по XHR и fetch прогнозируемо выполняются быстрее, чем базовое время dominteractive (вероятно, потому, что эти запросы почти всегда асинхронны). CSS и скрипты часто загружаются так, что препятствуют рендерингу, поэтому неудивительно, что они связаны с замедлением time-to-interactive. Видео относительно малозатратно.

Выводы


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

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

Библиотеки


Чтобы разобраться, какие библиотеки используются на странице, мы воспользовались следующим подходом: на каждом сайте мы фиксировали глобальные переменные (например, свойства объекта окна). Далее каждая глобальная переменная, встречавшаяся более шести тысяч раз связывалась (когда это было возможно) с библиотекой JavaScript. Это очень кропотливая работа, но поскольку в наборе данных также содержались запрашиваемые URL для каждой страницы, мы могли изучить пересечение встречаемых переменных и запросов URL, и этого часто было достаточно, чтобы определить, какая библиотека задаёт каждую из глобальных переменных. Глобальные переменные, которые нельзя было с уверенностью связать с какой-то одной библиотекой, игнорировались. Такая методология в определённой мере обеспечивает неполный учёт: библиотеки JS не обязаны оставлять что-то в глобальном пространстве имён. Кроме того, она обладает некоторым шумом, когда разные библиотеки задают одно и то же свойство, и этот факт при привязке библиотек не учитывался.

Какие библиотеки JavaScript используются сегодня наиболее часто? Если следить за темами конференций и постов, было бы вполне логично предположить, что это React, Vue и Angular. Однако в нашем рейтинге они совершенно далеки от вершины.

10 самых используемых библиотек



Просмотреть полный список библиотек по уровню использования

Да, на вершине находится старый добрый jQuery. Первая версия JQuery появилась в 2006 году, то есть 14 человеческих лет назад, но в годах JavaScript это гораздо больше. Если измерять в версиях Angular, то это произошло, вероятно, сотни версий назад. 2006 год был совершенно иным временем. Самым популярным браузером был Internet Explorer 6, крупнейшей социальной сетью MySpace, а скруглённые углы на веб-страницах были такой революцией, что люди называли их Веб 2.0. Основная задача JQuery обеспечение кроссбраузерной совместимости, которая в 2020 году совершенно отличается от ситуации 2006 года. Тем не менее, 14 лет спустя аж половина веб-страниц из нашей выборки загружала jQuery.

Забавно, что 2,2% веб-сайтов выбрасывали ошибку, потому что JQuery не загружался.

Судя по этой десятке лучших, наши браузеры в основном выполняют аналитику, рекламу и код для совместимости со старыми браузерами. Почему-то 8% веб-сайтов определяют полифил setImmediate/clearImmediate для функции, реализация которой даже не планируется ни в одном из браузеров.

Прогнозирование time-to-interactive по использованию библиотек


Мы снова запустим линейную регрессию, прогнозирующую dominteractive по наличию библиотек. Входящими данными регрессии будет вектор X, где X.length == количество библиотек, где X[i] == 1.0, если библиотека i присутствует, X[i] == 0.0, если её нет. Разумеется, мы знаем, что на самом деле dominteractive не определяется наличием или отсутствием конкретных библиотек. Однако моделирование каждой библиотеки как имеющей вклад в замедление и выполнение регрессии для сотен тысяч примеров всё равно позволяет нам обнаружить интересные находки.

Наилучшие и наихудшие для time-to-interactive библиотеки по коэффициентам регрессии



Посмотреть полный список библиотек по коэффициентам регрессии, прогнозирующей dominteractive

Отрицательные коэффициенты означают, что модель прогнозирует меньшее time-to-interactive при наличии этих библиотек, чем при их отсутствии. Разумеется, это не означает, что добавление этих библиотек ускорит ваш сайт, это только значит, что сайты с этими библиотеками оказываются быстрее, чем некая базовая линия, установленная моделью. Результаты могут быть столь же социологическими, сколь и техническими. Например библиотеки для ленивой загрузки по прогнозу обеспечивают снижение time-to-interactive. Возможно, это просто вызвано тем, что страницы с этими библиотеками созданы программистами, потратившими время на оптимизацию с целью быстрой загрузки страниц, поскольку ленивая загрузка приводит непосредственно к этому. Мы не можем отвязать подобные факторы от данной структуры.

Наилучшие и наихудшие для времени onload библиотеки по коэффициентам регрессии


Мы можем повторить описанный выше процесс, но на этот раз для прогнозирования onloadtime. Onloadtime это время, которое требуется для срабатывания события окна load, то есть время, требуемое для загрузки всех ресурсов страницы. Проведём линейную регрессию точно таким же образом, как и ранее.


Посмотреть полный список библиотек по коэффициентам регрессии, прогнозирующей onloadtime

Наилучшие и наихудшие библиотеки для jsheapusedsize по коэффициентам регрессии


Это прогноз для размера в мегабайтах кучи, используемой JavaScript.


Посмотреть полный список библиотек по коэффициентам регрессии, прогнозирующей jsheapusedsize

Комментаторы в Интернете с любят высокомерно говорить, что корреляция не равна причинно-следственной связи, и мы в самом деле не можем напрямую вывести из этой модели причинности. При интерпретировании коэффициентов следует быть очень аккуратными, частично это вызвано тем, что может участвовать множество искажающих факторов. Однако этого определённо достаточно для того, чтобы задуматься. Тот факт, что модель связывает замедление time-to-interactive на 982 мс с наличием jQuery, и что половина сайтов загружает этот скрипт, должен навести нас на определённые мысли. Если вы оптимизируете свой сайт, то сверка его списка зависимостей с представленными здесь рейтингами и коэффициентами определённо обеспечит вам приличный показатель того, устранение какой зависимости даст вам наибольший рост производительности.

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



На правах рекламы


VDS для проектов и задач любых масштабов это про наши эпичные серверы! Новейшие технологии и оборудование, качественный сервис. Поспешите заказать!

Подробнее..

Производительность Android Runtime vs NDK

16.05.2021 12:20:18 | Автор: admin

Разрабатывая игровой движок для Android, я был уверен, что нативный код C/C++ будет исполняться быстрее чем аналогичный код на Java. Это утверждение справедливо, но не для последних версий Android. Чтобы проверить почему так происходит, решил провести небольшое исследование.

Для теста использовался Android Studio 4.1.3 - для Java Android SDK (API 30), для C/C++ Android NDK (r21, компилятор CLang). Тест довольно тупой, который выполняет арифметические операции над массивом int в двух вложенных циклах. Ничего осмысленного и специфичного.

Вот метод, написанный на Java:

public void calculateJava(int size) {    int[] array = new int[size];    int sum = 0;    for (int i=0; i<size; i++) {        array[i] = i;        for (int j=0; j<size; j++) {            sum += array[i] * array[j];            sum -= sum / 3;       }    }     }

Вот метод на C/C++ (осознанно не освобождаю память для чистоты сравнения с Java GC):

extern "C" JNIEXPORT void JNICALL Java_com_axiom_firstnative_MainActivity_calculateNative(        JNIEnv* env,        jobject,        jint size) {    int* array = new int[size];    int sum = 0;    for (int i=0; i<size; i++) {        array[i] = i;        for (int j=0; j<size; j++) {            sum += array[i] * array[j];            sum -= sum / 3;        }    }    // delete[] array;}

Вот метод, который вызывает тесты Java:

     long startTime = System.nanoTime();     calculateNative(4096);     long nativeTime = System.nanoTime() - startTime;                     startTime = System.nanoTime();     calculateJava(4096);     long javaTime = System.nanoTime() - startTime;                     String report = "VM:" + System.getProperty("java.vm.version")                        + "\n\nC/C++: " + nativeTime                         + "ns\nJava: " + javaTime + "ns\n"                        + "\nJava to C/C++ ratio "                         + ((double) javaTime / (double) nativeTime);

Вот такие результаты получились на разных устройствах

Samsung Galaxy Tab E (Android 4.4.4) :
Java time: 2 166 748 ns
C/C++ time: 396 729 ns (C/C++ быстрее в 5 раз )

Prestigio K3 Muze (Android 8.1):
Java time: 3 477 001ns (первый запуск)
C/C++ time: 547 692ns (C/C++ в 6 раз быстрее),
НО при повторном запуске теста Java выполняется лишь на 30-40% медленнее (разогрев?).

Samsung Galaxy S21 Ultra (Android 11):
Java time: 111 000ns
C/C++ time: 121 269ns
Интересно: Java на 9% медленнее при первом запуске и 40-50% быстрее C/C++ при втором.

Включение флагов оптимизации компилятора CLang (-O3) делают кодC/C++ быстрее на ~30-35% (Prestigio K3 Muze Android 8.1) чем Java код, даже при втором запуске.

Но на Smasung Galaxy S21 Ultra (Android 11) Java код выполняется на 10-20% быстрее чем нативный С/C++ код скомпилированный CLang с включенными флагами оптимизации (-O3). Это чуток взорвало мой мозг...

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

И тут возник вопрос, почему Java код на последних Android устройствах выполняется быстрее аналогичного C/C++ кода? Как это вообще возможно?

А возможно это следующим способом. В Android Runtime наряду с Ahead-of-Time компиляцией Java кода в нативный код, по прежнему работает механизм Just-In-Time компиляции на основе собранной статистики первого запуска приложения. Где имея данные о часто выполняемых участках кода можно провести более эффективные оптимизации. Вот так примерно говорят в документации:

The JIT compiler complements ART's current ahead-of-time (AOT) compiler and improves runtime performance. Although JIT and AOT use the same compiler with a similar set of optimizations, the generated code might not be identical. JIT makes use of runtime type information can do better inlining and makes on stack replacement (OSR) compilation possible, all of which generate slightly different code.

Есть ли смысл переписывать приложения на Java на NDK C/C++ для производительности?

Как мне кажется, для старых устройств с виртуальной машиной Dalvik VM (до 7.0 версии Android) - однозначно, да. Что касается более новых устройств, с Android версии выше 7.0 (где используется среда выполнения ART), это не имеет большого смысла, если только вы не опытный С/C++ разработчик, глубоко понимающий как работает CPU и способный сделать оптимизации лучше Android Runtime. Да и игра не стоит свеч (эффект/усилия), за исключением следующих случаев:

  • Вы портируете имеющееся приложение на C/C++ на Android

  • Вы хотите использовать С/C++ библиотеки не доступные Java

  • Вы хотите использовать API не доступные в Android SDK

P.S. Если у вас есть какие-то соображения, буду рад комментариям.

Подробнее..

Вам показалось! Все о Perceived Performance

21.01.2021 14:15:10 | Автор: admin

Перед вами быстрый, удобный и отзывчивый сайт? Возможно, это не результат плодотворной работы множества людей, а всего лишь набор психологических и инженерных трюков, направленных на улучшение Perceived Performance.

В большинстве случаев с ростом реальной производительности улучшается и Perceived Performance. А когда реальная производительность не может быть с легкостью увеличена, существует возможность поднять видимую. В своем докладе на Frontend Live 2020 бывший разработчик Avito Frontend Architecture Алексей Охрименко рассказал о приемах, которые улучшают ощущение скорости там, где ускорить уже нельзя.

Производительность обязательное условие успеха современных Web-приложений. Есть множество исследований от Amazon и Google, которые говорят, что понижение производительности прямо пропорционально потере денег и ведет к слабой удовлетворенности вашими сервисами (что снова заставляет терять деньги).

Первопричина полученного стресса ожидание. Научно доказано, что оно может вызывать:

  • беспокойство;

  • неуверенность;

  • дискомфорт;

  • раздражение;

  • скуку.

Вряд ли кто-то хочет, чтобы пользователи испытывали эти эмоции. Поэтому так важно уменьшать время ожидания.

Самое простое решение все делать быстро. Но прежде, чем вы скажете: Спасибо, капитан!, хочу напомнить, что увеличение производительности ведет к прямому уменьшению времени ожидания.

Не игнорируйте этот момент: вы обязательно должны его учитывать.

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

  • lighthouse;

  • devTools profiler.

Но даже если все сделать идеально, этого может оказаться недостаточно.

Есть интересная история про аэропорт Huston. Она отлично вписывается и в современные реалии разработчиков.

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

Люди часто жаловались на долгое ожидание багажа в аэропорту. И инженеры аэропорта Huston не спали ночами, работали сверхурочно, но реализовали выдачу багажа всего за 8 минут. Однако клиенты все равно продолжали жаловаться.

Инженеры не знали, что делать. И они решили попробовать один трюк. Высадив людей как можно дальше от зоны выдачи багажа, они вынудили их быть в активном ожидании. Пока те добирались до места назначения, багаж уже ждал их. В результате все жалобы прекратились. Но самое интересное, что реальное время выдачи багажа осталось абсолютно таким же: 8 минут.

Почему же клиенты пришли в восторг от происходящего, хотя реальная производительность никак не изменилась?

Perceived Performance

Инженеры аэропорта улучшили Perceived Performance, или видимую производительность. Это очень глубокий термин. Видимая производительность включает в себя реальную производительность.

Если вы ускорите работу своего сайта, увидите, что он ускорился абсолютно логичная закономерность.

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

Все это базируется на том, что люди не очень хорошо умеют измерять реальную производительность. И именно эту особенность можно эксплуатировать.

Все дальнейшие примеры будут для Angular приложений, но, несмотря на это, они применимы к любому современному фреймворку. Приступим!

Не блокируйте пользователя

Первый совет: ни в коем случае не стоит блокировать пользователя. Вы можете сейчас сказать: Я и не блокирую!.

Попробуйте узнать себя в этом примере:

Допустим, мы кликаем на удаление какого-то элемента, запрашиваем данные, после чего удаляем.

Все равно не узнаете? А так?

Спиннеры это не выход! Хоть они и могут стать ленивым способом разобраться с неудобной ситуацией.

Что можно предложить в качестве альтернативы спиннеру?

Можно нажать на кнопку УДАЛИТЬ и показать статус этой кнопки (item удаляется только для одного элемента), не демонстрируя спиннер. В дальнейшем можно отправить запрос на сервер, и когда он придет, показать, что элемент удалился, либо при ошибке передать данные о ней. Вы можете возразить: Но я могу делать только 1 запрос за раз! это ограничение бэкенда. С помощью RxJs и оператора concat можно буквально одной строчкой кода создать минимальную очередь:

Более серьезная имплементация, конечно, займет больше, чем одну строчку.

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

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

В Angular есть ngx-spinner, который поддерживает такой функционал.

Это, как минимум, уменьшит время ожидания, и пользователь сможет в этот момент сделать хоть что-то.

Обманывайте

Обман зачастую базируется на иллюзиях скорости.

Допустим, есть два спиннера с точками. Если увеличить количество точек в одном из них, кажется, что он начинает крутиться быстрее. На самом деле скорость спиннеров одинакова.

Иллюзия базируется на простом эффекте: чем больше точек, тем больше Motion Blur (размытие движения). Этот трюк используется в мультфильмах. Возможно, вы видели, что когда койот бежит очень быстро, у него размываются ноги, и вместо них появляется облако.

Где можно применить такую методологию?

  • Progress Bar;

Есть исследование, показывающее, что если добавить полоски, которые идут в обратном направлении внутри Progress Bar, то визуально он выглядит как более быстрый. В такой настройке можно получить до 12% ускорения, просто применив дополнительный скин. И пользователи воспримут работу, прошедшую под этим Progress Bar, на 12% быстрее. Вот пример того, как можно реализовать такой слайдер на CSS.

  • Скелетон;

Если спиннеры это плохо, что же тогда показать на экране? Можно продемонстрировать силуэт, некий образ того контента, который будет отображен.

Скелетон это некое схематическое отображение сайта до момента его загрузки:

В этом примере мы видим, где будут расположены чаты.

Существует исследование, которое показывает, что люди воспринимают скелетоны быстрее от 10 до 20%.

То есть по ощущениям пользователей, сайты со скелетоном работают быстрее.

Существует огромное количество нужных компонентов для Angular, React, View. К примеру, для Angular есть skeleton-loader, в котором можно прописать внешний вид и сконфигурировать его. После чего мы получим наш скелетон:

  • Экспоненциальная выдержка.

Следующее, что необходимо проявлять это не обычную выдержку, а экспоненциальную. Optimistic Updates имеют одну проблему: мы обязаны откатить состояния приложения назад в случае наличия проблемы с запросом и показать сообщение об ошибке. Но эту ситуацию можно сгладить. Давайте посмотрим на простой пример с Optimistic Update:

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

Это одна из best practice в энтерпрайз-приложениях, потому что бэкенд может не работать по разным причинам. Например, происходит деплой или хитрая маршрутизация. В любом случае очень хорошее решение: попробовать повторить. Ведь никакое API не дает 100% гарантии, 99,9% uptime.

Но есть маленький нюанс. Если мы будем повторять бесконечно, то в какой-то момент повалим наш сервер. Поэтому обязательно поставьте ограничение. Например, повторять максимум 3 раза.

Но даже с этим сценарием мы сами себе можем сделать DDOS (Distributed Denial of Service). На это попадались многие компании. Например, Apple с запуском своего сервиса MobileMe.

В чем идея? Представьте на секунду, что ваш сервис падает под нагрузкой, то есть он с ней не справляется. Если сервер перегружен, то скорее всего ответит статус-кодом 500. Если вы попробуете еще раз, возможно, следующий запрос получит ответ.

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

Best practice: применять exponential backoff. В rxjs есть хороший дополнительный npm пакет backoff-rxjs, который за вас имплементирует данный паттерн.

Имплементация очень простая, 10 строчек кода. Здесь вы можете обойтись одной. Указываете интервал, через который начнутся повторы, количество попыток, и сбрасывать ли увеличивающийся таймер. То есть вы увеличиваете по экспоненте каждую следующую попытку: первую делаете через 1 с, следующую через 2 с, потом через 4 с и т.д.

Играя с этими настройками, вы можете настраивать их под ваше API.

Следующий совет очень простой добавить Math.random() для initialInterval:

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

Предугадывайте!

Как уменьшить ожидание, когда невозможно ускорить процесс?

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

  • Предзагрузка картинок;

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

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

  • Предзагрузка 18+

Наверняка вы сталкивались в HTML со стеком link, который позволяет переподключить stylesheets:

Немного поменяв атрибуты, мы можем применить его для предзагрузки:

Можно указать атрибут rel ="preload", сослаться в ссылке на наш элемент (href="styles/main.css"), и в атрибуте as описать тип предзагружаемого контента.

  • prefetch.

Еще один вариант это prefetch:

Главное запомнить, что preload и prefetch два самых полезных инструмента. Отличие preload от prefetch в том, что preload заставляет браузер делать запрос, принуждает его. Обычно это имеет смысл, если вы предзагружаете ресурс на текущей странице, к примеру, hero images (большую картинку).

ОК, это уже лучше, но есть одна маленькая проблема.

Если взять какой-нибудь среднестатистический сайт и начать префетчить все JavaScript модули, то средний рост по больнице составляет 3 МБ. Если мы будем префетчить только то, что видим на странице, получаем примерно половину 1,2 МБ. Ситуация получше, но все равно не очень.

Что же делать?

Давайте добавим Machine Learning

Сделать это можно с помощью библиотеки Guess.js. Она создана разработчиками Google и интегрирована с Google Analytics.

Анализируя паттерны поведения пользователей и, подключаясь к вашему приложению и системе сборки, она может интеллектуально делать prefetch только 7% на странице.

При этом эта библиотека будет точна на 90%. Загрузив всего 7%, она угадает желания 90% пользователей. В результате вы выигрываете и от prefetching/preloading, и от того, что не загружаете все подряд. Guess.js это идеальный баланс.

Сейчас Guess.js работает из коробки с Angular, Nuxt js, Next.js и Gatsby. Подключение очень легкое.

Поговорим о Click-ах

Что можно сделать, чтобы уменьшить ожидание?

Как предугадать, на что кликнет пользователь? Есть очевидный ответ.

У нас есть событие, которое называется mousedown. Оно срабатывает в среднем на 100-200 мс раньше, чем событие Click.

Применяется очень просто:

Просто поменяв click на mousedown, мы можем выиграть 100-200 мс.

Мы можем заранее предсказать, что пользователь кликнет на ссылку.

Я пытаюсь кликнуть на ссылку, и сайт мне говорит, что он знал об этом на 240-500 мс раньше того, как я это сделал.

Опять магия ML? Нет. Существует паттерн: когда мы хотим на что-то кликнуть, мы замедляем движение (чтобы было легче попасть мышкой на нужный элемент) .

Есть библиотека, которая анализирует скорость, и благодаря этому может предсказать, куда я кликну.

Библиотека называется futurelink. Ее можно использовать абсолютно с любым фреймворком:

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

Что пользователь хочет получить при переходе на страницу? В большинстве сценариев: HTML, CSS и немного картинок.

Все это можно реализовать за счет серверного рендеринга SSR.

В Angular для этого достаточно добавить одну команду:

ng add @nguniversal/express-engine

В большинстве случаев это работает замечательно, и у вас появится Server-Side Rendering.

Но что, если вы не на Angular? Или у вас большой проект, и вы понимаете, что внедрение серверного рендеринга потребует довольно большого количества усилий?

Здесь можно воспользоваться статическим prerender: отрендерить страницы заранее, превратить их в HTML. Для этого есть классный плагин для webpack, который называется PrerenderSPAPlugin:

В нем нужно указываете директорию, куда вы хотите сохранять пререндеренные urls, и перечислить те, для которых нужно отрендерить контент заранее.

Но вы можете сделать все еще проще: зайти в свое SPA приложение и написать:

document.documentElement.outerHTML,

получить пререндеренный HTML и воспользоваться им: сохранить это в файл. И вот у вас за пару минут появилась пререндеренная страница. Если ваша страница меняется очень редко, это неплохой вариант (как бы глупо он ни выглядел).

Заключение

Несмотря на то что Perceived Performance очень субъективная метрика, ее можно и нужно измерять. Об этом говорилось в докладе Виктора Русаковича на FrontendConf 2019.

Он рассказывал о том, что в Скелетоне есть анимация в плавном фоне, и слева направо она воспринимается на 68% быстрее, чем справа налево. Есть разные исследования, которые показывают, что неправильно примененные техники Perceived Performance могут визуально сделать сайт хуже. Поэтому обязательно тестируйте.

Сделать это можно при помощи сервиса под названием Яндекс.Толока.

Он позволяет выбирать, какой из двух объектов лучше. Можно сделать два видео с разной анимацией и предложить пользователям оценить, какой из них быстрее. Это стоит недорого, скейлится очень хорошо. В сервисе зарегистрировано огромное количество людей из разных регионов, то есть можно делать разнообразные выборки по разным параметрам. Благодаря этому есть возможность провести серьезное UX-исследование за небольшую сумму.

Даже если в конце концов вашему начальнику покажется, что быстрее не стало, проведите исследование и попробуйте улучшить ситуацию с помощью Perceived Performance.

Perceived performance это низковисящий фрукт почти в любой компании. Именно сейчас у вас есть возможность, применив вышеперечисленные техники, улучшить воспринимаемую производительность и сделать пользователей и менеджеров счастливыми.

Конференция, посвященная всем аспектам разработки клиентской части веб проектов, FrontendConf 2021 пройдет 29 и 30 апреля. Билеты можно приобрести здесь. Вы можете успеть купить их до повышения цены!

А для того, чтобы стать одним из героев конференции, подайте доклад.

Черновик

Подробнее..

Перевод Вам не нужны ни PWA, ни AMP, чтобы ваш сайт загружался быстро

04.05.2021 16:09:09 | Автор: admin
image

Знаменитая страничка Airbnb на 800Kb. Я ожидал бы большей заботы о производительности от 900+ разработчиков со средней зарплатой 290 000 долларов в год. Даже SublimeText в какой-то момент перестает выделять эту чушь.

В сети появилась тенденция к появлению новых революционных методов, которые позволяют делать вещи, которые были возможны десятилетия назад.

AMP


Во-первых, AMP (ускоренные мобильные страницы). Подумайте вот о чем: в целом Интернет не может быть быстрым, поэтому Google изобретает параллельный Интернет, в котором вам просто не разрешают использовать JavaScript. Ах да, и они позволяют вам использовать пару одобренных Google компонентов AMP JS. Но подождите, может ли обычный Интернет работать без JavaScript? Конечно может. Может ли обычный Интернет включать пользовательские компоненты JS? Не сомневайтесь. Это может быть быстро? Netflix недавно обнаружил, что если они удалят 500 КБ JavaScript со статической (!!!) веб-страницы, она будет загружаться НАМНОГО быстрее, и пользователи в целом будут счастливее. Кто бы мог подумать, правда?

Так зачем был нужен AMP? Ну, в основном Google нужно было заблокировать поставщиков контента, которые будут обслуживаться через поиск Google. Но для этого им нужна была хорошая легенда. И они решили продвигать его как решение для повышения производительности.

Дело в том, что веб-разработчики не верят в производительность. Они говорят, что верят, но на самом деле это не так. Они верят в ажиотаж. Поэтому, если вы рекламируете старые трюки под новым именем, разработчики могут сказать: Теперь, наконец, я могу начать писать быстрые приложения. Спасибо, Google! . Например, если бы Google когда-либо мешал вам делать это заранее.

Но AMP новый! <amp-img> делает гораздо больше, чем !


Возможно, но что мешает Google, если он действительно намеревается помочь, выпустить его как обычную JS-библиотеку?

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

До этого:

Привет, босс, давайте перепишем наш веб-сайт, чтобы он загружался быстрее!
Отвали!
Но исследования показывают, что каждая секунда загрузки
Я сказал, отвали!


Сейчас:

Привет, босс, давайте перепишем наш сайт с помощью AMP. Это новая технология от Google ..."
Брось все! Вот возьми $$$
Это также может улучшить
Мне все равно. Начни это СЕЙЧАС!


Я не говорю, что методы, продвигаемые AMP, плохие или бесполезные. Это хорошие практики. Но ничто не мешает вам следить за ними в обычной сети. Ничто никогда не мешало вам писать эффективные страницы с самого зарождения Интернета. Google почти не изобрел CDN и загрузку асинхронных скриптов. Но никого это не волновало, потому что старые технологии и передовой опыт никогда не были столь заманчивыми, как что-то, называемое новым.

PWA


Войдите в PWA. Прогрессивные веб-приложения. Или приложения. Прогрессивные веб-приложения. Что бы это ни было.

Итак, идея заключалась в том, чтобы иметь возможность создать нативный опыт, но с веб-стеком. Чего не хватало сети? Установки приложений. Автономного режима. Уведомления (Ew). Работы в фоновом режиме. Да, в основном все. Это все.

Опять же, я не собираюсь говорить, что это неправильно. 1. Это не так. Если вы хотите создать нативное приложение с использованием веб-технологий, вам придется использовать что-то подобное. И это имеет смысл для приложений, таких как список покупок или, я не знаю, будильник?

Проблема с PWA в том, что есть две проблемы.

Во-первых, большинство приложений было бы лучше использовать в качестве веб-сайтов, чем приложений. Веб-сайты загружают каждый ресурс постепенно по мере необходимости, в отличие от приложений, которые должны получать все при установке (поэтому размеры пакетов приложений обычно намного больше, чем у веб-сайтов). Сайты более эффективны, но вы не можете использовать их в автономном режиме.

Но большинство приложений сегодня в любом случае доступны только в сети! Вы не можете позвонить в Uber, находясь в офлайн-режиме. Иначе зачем вам открывать приложение Uber? Tinder бесполезен в автономном режиме. Вы не можете встречаться с пустыми экранами чата. Вы не можете присоединиться к встрече на Meetup.com без подключения к сети. Вы не можете выбрать или забронировать отель, вы не можете перевести деньги или проверить баланс своего счета в автономном режиме. И никто не хочет перечитывать старые кешированные твиты из Twitter или вчерашние фотографии из Instagram. В этом нет никакого смысла.

Так что да, я бы предпочел, чтобы эти приложения были просто веб-сайтами. Вы не поверите, но в этом есть свои преимущества. Мне нравится меньший размер загружаемых файлов, особенно если я иногда захожу на сайт просто для того, чтобы быстро его просмотреть. Мне нравится, что веб-сайты не потребляют мои ресурсы в фоновом режиме. 2. Когда я закрываю его, он выгружается и не загружает постоянно новые версии собственных библиотек, которые разработчикам часто необходимо развертывать. Ради этого я более чем готов пожертвовать офлайн-режимом.

Вторая проблема с PWA, более актуальная для нашей темы, заключается в том, что она каким-то образом связана с производительностью.

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

Поэтому самостоятельное управление кешем ресурсов в ServiceWorker кажется скорее обузой, чем благословением. HTTP-кеширование также декларативно, хорошо протестировано и хорошо изучено на данный момент, иными словами, его трудно испортить. Чего вы не можете сказать о своем ServiceWorker. Кэширование одна из двух самых сложных вещей в компьютерных науках. У меня лично был плохой опыт работы с Meetup.com PWA, когда из-за ошибки в их коде кеша весь сайт стал непригодным для использования до такой степени, что не открывались страницы встреч. И, в отличие от HTTP, его не так просто сбросить. Нет, обновление не помогло.

Но было бы нормально, если бы ServiceWorker был компромиссом: вы платите за сложность, но получаете новые захватывающие возможности. За исключением того, что вы этого не получите. Нет ничего полезного, что вы можете сделать с ServiceWorker, чего вы не сможете сделать с HTTP-кешем / AJAX / REST / локальным хранилищем. Это просто дыра сложности, в которую вы потратите бесчисленное количество рабочих часов.

PWA, как и AMP, даже не гарантирует, что ваш веб-сайт будет хоть сколько-нибудь быстрым или мгновенным. Забавно, как в тематическом исследовании Tinder показано, что экран входа в систему (один ввод текста, одна кнопка, один логотип SVG и фоновый градиент) загружается через 4G-соединение за 5 секунд! Я имею в виду, что им пришлось добавить загрузчик на 2-5 секунд, чтобы пользователи сразу не закрывали эту херню. И они называют это быстро.

Настолько это быстро:



Как они это делают? Чертовски заботясь о производительности. Так просто.

О, также не обслуживают миллионы пакетов JavaScript и не выполняют рендеринг на клиенте с React, обслуживаемым через GraphQL с помощью fetch polyfill. Это, вероятно, тоже помогло.

image

ServiceWorker или AMP, если ваша целевая страница содержит 170+ запросов на 3,1 Мб для изображения и четырех полей формы, она не может загружаться быстро, независимо от того, сколько новых фреймворков вы добавите в нее.

Вердикт


Так какой вердикт? Чтобы писать быстрые веб-сайты с помощью AMP и PWA, вам все равно нужно глубоко разбираться в оптимизации производительности. Без этого единственный выбор, который у вас есть, это гнаться за ажиотажем.

Но помните, что ни AMP, ни PWA волшебным образом не сделают ваш сайт быстрее, чем, скажем, обычная перезапись.

Однако, как только вы разберетесь с производительностью, вы заметите, что вам не нужны ни AMP, ни PWA. Просто перестаньте заниматься ерундой, и Интернет внезапно начнет работать мгновенно. AMP не изобрела CDN и . PWA не изобрела кеширование. Статическая сеть по-прежнему вращается вокруг любой современной широко разрекламированной среды.

image

Но пользователи! Им нужна наша модная интерактивность. Они ТРЕБУЮТ анимации!

Скажу одно. Никто не любит смотреть на экран загрузки 5 секунд. Анимированный загрузчик не имеет значения. Если вы не можете дать производительность, по крайней мере, не притворяйтесь, что это некая особенность.

________________________________

  1. Хотя я не думаю, что нам нужно больше уведомлений в нашей жизни. Особенно со случайных посещаемых нами веб-страниц. Даже не из собственных приложений я держу свой телефон в постоянном режиме Не беспокоить с коротким списком приложений, которые могут отправлять уведомления.
  2. Кстати, с тех пор, как вы впервые открыли эту статью, мой ServiceWorker в фоновом режиме загрузил 0 Кб бесполезных данных. Надеюсь, у вас есть WiFi :)
Подробнее..

Оптимизация .NET приложения как простые правки позволили ускорить PVS-Studio и уменьшить потребление памяти на 70

15.06.2021 18:13:51 | Автор: admin

Проблемы с производительностью, такие как аномально низкая скорость работы и высокое потребление памяти, могут быть обнаружены самыми разными способами. Такие недостатки приложения выявляются тестами, самими разработчиками или тестировщиками, а при менее удачном раскладе пользователями. Увы, но обнаружение аномалий лишь первый шаг. Далее проблему необходимо локализовать, ведь в противном случае решить её не получится. Тут возникает вопрос как найти в большом проекте причины, приводящие к излишнему потреблению памяти и замедлению работы? Есть ли они вообще? Быть может, дело и не в приложении вовсе? Эта статья посвящена истории о том, как разработчики C#-анализатора PVS-Studio столкнулись с подобной проблемой и смогли решить её.

Бесконечный анализ

Анализ крупных C#-проектов всегда занимает некоторое время. Это ожидаемо PVS-Studio погружается в исследование исходников достаточно глубоко и использует при этом различные технологии, такие как межпроцедурный анализ, анализ потока данных и т.д. Тем не менее анализ многих крупных проектов, найденных нами на github, производится не дольше нескольких часов.

Возьмём, к примеру, Roslyn. Его solution включает более 200 проектных файлов, и почти все из них проекты на C#. Нетрудно догадаться, что в каждом из проектов далеко не по одному файлу, а сами файлы состоят далеко не из пары строчек кода. PVS-Studio проводит полный анализ Roslyn примерно за 1,5-2 часа. Конечно, некоторые проекты наших пользователей требуют гораздо больше времени на анализ, но ситуации, когда анализ не проходит даже за сутки, исключительны.

Именно в такой ситуации и оказался один из наших клиентов. Он написал в поддержку, что анализ его проекта не проходит... Даже за 3 дня! Тут явно было что-то не так. Очевидно, мы не могли оставить подобную проблему без внимания.

Стоп, а как же тестирование?!

Наверняка у читателя возникает логичный вопрос почему же проблема не была выявлена на этапе тестирования? Как же так вышло, что она была обнаружена именно клиентом? Неужели C#-анализатор PVS-Studio не тестируется?

Тестируется и тщательно! Для нас тестирование является неотъемлемой частью процесса разработки. Корректность работы анализатора постоянно проверяется, ровно как проверяется и корректность работы отдельных его частей. Без преувеличения можно сказать, что unit-тесты диагностических правил и внутренних механизмов составляют примерно половину от общего объёма исходного кода проекта C#-анализатора. Кроме того, каждую ночь на сервере производится анализ большого набора проектов и проверка корректности формируемых анализатором отчётов. При этом автоматически определяется как скорость работы анализатора, так и объём потребляемой памяти. Более-менее существенные отклонения от нормы мгновенно обнаруживаются и исследуются.

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

Поиск причин

Дамп

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

Эти детали нам мог дать дамп памяти процесса анализатора. Что это такое? Если вкратце, то дамп это срез данных из оперативной памяти. С его помощью мы решили выяснить, какие данные загружены в рабочую память процесса PVS-Studio. В первую очередь нас интересовали какие-нибудь аномалии, которые могли стать причиной столь сильного замедления работы.

Мы попросили пользователя ещё раз запустить анализ своего проекта, подождать некоторое время, а затем сохранить дамп процесса и отправить его нам. Эти операции не требуют наличия каких-то особых программ или навыков дамп можно получить через самый обычный Диспетчер задач.

От файла с дампом мало толку, если нет возможности его открыть. К счастью, пользователю этим заниматься уже не нужно :). Ну а мы решили изучить данные дампа при помощи Visual Studio. Делается это достаточно просто:

  1. Открываем проект с исходниками приложения в Visual Studio.

  2. В верхнем меню нажимаем File->Open->File (или Ctrl+O).

  3. Находим файл с дампом и открываем.

В результате появится окошко с кучей различной информации о процессе:

Нас в первую очередь интересовала возможность перехода в своеобразный режим отладки дампа. Для этого нужно нажать кнопку Debug With Managed Only.

Примечание. Если вас интересует более подробная информация по теме открытия дампов через Visual Studio для отладки, то отличным источником информации будет официальная документация.

Итак, мы перешли в режим отладки. Отладка дампа достаточно мощный механизм, но важно помнить и о некоторых ограничениях:

  • отсутствует какая-либо возможность возобновления работы процесса, пошагового выполнения кода и т.п.;

  • в окне Quick Watch и Immediate Window невозможно использовать некоторые функции. К примеру, попытка вызова метода File.WriteAllText приводила к возникновению ошибки "Caracteres no vlidos en la ruta de acceso!". Дело в том, что дамп связан с окружением, на котором он был снят.

Отладка дампа позволила нам получить достаточно большое количество различных данных. Ниже представлена небольшая часть информации о состоянии процесса анализа в момент снятия дампа:

  • вычисленное количество файлов в проекте: 1 500;

  • приблизительное время анализа: 24 часа;

  • количество одновременно анализируемых в текущий момент файлов: 12;

  • количество уже проверенных файлов: 1060.

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

Тем не менее, понять причины замедления нам не удалось. Мы не обнаружили каких-либо аномалий или ошибок, а количество файлов в проекте не казалось чем-то из ряда вон выходящим. Анализ проекта похожего объёма обычно занимает у нас около 2 часов.

Конечно, на время анализа влияет не только размер проекта, но и сложность используемых в нём конструкций. Нам было известно, что большое количество циклов и высокий уровень вложенности приводит к замедлению анализа. Из дампа стало ясно, что в проекте пользователя действительно присутствовали подобные фрагменты. Однако даже усложнённая структура кода не должна была превращать двухчасовой анализ в... бесконечный!

Наконец-то, воспроизведение проблемы

Используя данные из дампа, мы поняли, что анализ "завис" на конкретных файлах со сложной структурой кода. Мы попросили их у клиента, надеясь воспроизвести возникшую проблему. Однако при анализе отдельных файлов проблема не воспроизвелась.

Мы решили пойти дальше и создать собственный тестовый проект с большим количеством сложных конструкций. Было крайне важно воспроизвести проблему локально это позволило бы сильно упростить дальнейший поиск её решения.

Мы создали свой тестовый проект, стараясь повторить следующие характеристики проекта пользователя:

  • количество файлов;

  • средний размер файлов;

  • максимальный уровень вложенности и сложность используемых конструкций.

Скрестив пальцы, мы запустили его анализ и...

Никаких замедлений. После стольких приложенных усилий мы так и не смогли воспроизвести проблему. Анализ сформированного проекта проходил за вполне адекватное время и успешно завершался. Ни зависаний, ни ошибок, ни каких-то аномалий. В такие моменты можно всерьёз задуматься а не решил ли пользователь над нами пошутить?

Казалось, что мы уже всё перепробовали. Казалось, что докопаться до правды не выйдет. А ведь мы бы и рады были заняться исследованием проблемы с замедлением! Мы бы и рады были одолеть её, наконец, порадовать клиента, порадоваться самим. Как ни крути, анализ проекта нашего пользователя не должен был зависать!

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

Неожиданно обнаружилось, что на компьютере одного из сотрудников анализ тестового проекта также никак не мог пройти. Он использовал ту же самую версию анализатора и проверял тот же самый проект. В чём же тогда было отличие?

А отличие было в железе. Точнее говоря, в ОЗУ.

Казалось бы, при чём тут ОЗУ?

Наши автоматизированные тесты проводятся на сервере с 32 Гб доступной оперативной памяти. На компьютерах сотрудников её объём различается, но везде есть по крайней мере 16 гигабайт, а у большинства 32 и более. Воспроизвести же баг удалось на ноутбуке, объём оперативной памяти которого составлял 8 Гб.

Возникает логичный вопрос к чему это всё? Мы же решали проблему замедления работы, а не высокого потребления памяти!

Дело в том, что высокое потребление памяти действительно может приводить к замедлению работы приложения. Это происходит в тех случаях, когда процессу не хватает памяти, установленной на устройстве. В таких случаях активируется особый механизм memory paging (другое название "swapping"). При его работе часть данных из оперативной памяти переносится во вторичное хранилище (диск). При необходимости система загружает данные с диска. Благодаря данному механизму приложения могут использовать оперативную память в большем объёме, чем доступно в системе. Увы, но у этого чуда есть своя цена.

Состоит эта цена в ощутимом сокращении скорости работы. Операции с жёстким диском производятся намного медленнее, чем работа с оперативной памятью. И именно swapping сильнее всего замедлял работу нашего анализатора.

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

Решаем проблему

dotMemory и диаграмма доминаторов

Мы использовали приложение dotMemory, разработанное компанией JetBrains. Это профилировщик памяти для .NET, который можно запускать как прямо из Visual Studio, так и в качестве отдельного инструмента. Среди всех возможностей dotMemory более всего нас интересовало профилирование процесса анализа.

Ниже представлено окно присоединения к процессу:

Сначала нужно запустить соответствующий процесс, затем выбрать его и начать профилирование с помощью кнопки "Run". Откроется новое окно:

В любой момент времени можно получить снимок состояния памяти. За время работы процесса можно сделать несколько таких снимков все они появятся на панели "Memory Snapshots":

Далее нам необходимо детально изучить полученный снимок (для этого нужно кликнуть по его идентификатору). Открывшееся окно будет содержать большое количество различных элементов:

Более полную информацию о работе с dotMemory, включая подробное описание представленных здесь данных, можно найти в официальной документации. Нам же была особенно интересна sunburst диаграмма, показывающая иерархию доминаторов объектов, эксклюзивно удерживающих другие объекты в памяти. Для перехода к ней необходимо открыть вкладку "Dominators".

Все указанные действия мы проделали с процессом анализа специально созданного тестового проекта. Диаграмма доминаторов для него выглядела следующим образом:

Объекты высокого уровня были неособенно интересны, ведь сами по себе они не занимали много места. Куда важнее было узнать, что именно содержится "внутри". Какие же объекты размножились настолько, что начали занимать так много места?

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

Анализ потока данных (Data-Flow Analysis) заключается в вычислении возможных значений переменных в различных точках компьютерной программы. Например, если ссылка разыменовывается и при этом известно, что в текущий момент она может быть равна null, то это потенциальная ошибка, и статический анализатор сообщит о ней. Подробнее об этой и других технологиях, использующихся в PVS-Studio, можно прочитать в статье.

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

Что же тогда делать? Неужели опять тупик?

А не такие уж они и разные

Итак, вычисленные значения переменных кешируются, и их очень много. Настолько много, что проект не проверяется даже за 3 дня. Отказаться от кеширования этих значений мы не можем. Но что, если как-то оптимизировать способ их хранения?

Мы решили повнимательнее взглянуть на значения в кеше. Оказалось, что PVS-Studio хранил большое количество абсолютно идентичных объектов. К примеру, для многих переменных анализатор не может вычислить значение, так как оно может быть любым (в пределах ограничений своего типа):

void MyFunction(int a, int b, int c ....){  // a = ?  // b = ?  // c = ?  ....}

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

Идея родилась мгновенно нужно было всего лишь избавиться от дублирования. Правда реализация такого механизма потребовала бы от нас внесения большого количества сложных правок...

А вот и нет! На самом деле, нужно совсем немного:

  • некоторое хранилище, в котором будут находиться уникальные значения переменных;

  • механизмы доступа к хранилищу добавление новых и получение существующих элементов;

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

Изменения в отдельных частях анализатора затрагивали, как правило, пару строк. Реализация хранилища также не заняла много времени. В результате кеш стал хранить только уникальные значения.

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

Кроме того, можно вспомнить и такое понятие, как интернирование строк. По сути то же самое: если строки одинаковы по значению, то фактически они будут представлены одним и тем же объектом. В C# строковые литералы интернируются автоматически. Для прочих строк можно использовать методы string.Intern и string.IsInterned. Однако не всё так просто. Даже этим механизмом нужно пользоваться с умом. Если вам интересна данная тема, то предлагаю к прочтению статью "Подводные камни в бассейне строк, или ещё один повод подумать перед интернированием экземпляров класса String в C#".

Выигранная память

Мы внесли несколько мелких правок, реализовав паттерн Flyweight. Каковы были результаты?

Они были невероятны! Пиковое потребление оперативной памяти при проверке тестового проекта уменьшилось с 14,55 до 4,73 гигабайт. Столь простое и быстрое решение позволило уменьшить расход памяти примерно на 68%! Мы были шокированы и очень довольны результатом. Доволен был и клиент теперь ОЗУ его компьютера хватало, а значит, и анализ начал проходить за адекватное время.

Достигнутый результат действительно радовал, но...

Нужно больше оптимизаций!

Да, мы смогли уменьшить потребление памяти. Однако изначально мы же хотели ускорить анализ! Конечно, он действительно ускорился у клиента, как и на других машинах, где не хватало ОЗУ. Но ускорения на мощных компьютерах мы не добились только сократили потребление памяти. А раз уж мы столь глубоко погрузились в эту тему... Почему бы не продолжить?

dotTrace

Итак, мы решили отыскать дополнительные возможности для оптимизации. В первую очередь, нам было интересно какие части приложения работают дольше всего? Какие именно операции отнимают время?

Ответы на наши вопросы мог дать dotTrace хороший профилировщик производительности для .NET приложений, предоставляющий ряд интересных возможностей. Интерфейс этого приложения довольно сильно напоминает dotMemory:

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

Итак, используя dotTrace, мы запустили анализ одного большого проекта. Ниже показан пример окна, отображающего в реальном времени графики использования процессом памяти и CPU:

Чтобы начать "запись" данных о работе приложения, нужно нажать Start (по умолчанию процесс сбора данных начинается сразу). Подождав некоторое время, нажимаем "Get Snapshot And Wait". Перед нами отображается окно с собранными данными. Например, для простого консольного приложения это окно выглядит так:

Здесь нам доступно большое количество различной информации. В первую очередь интересно время работы отдельных методов. Также может быть полезно узнать время работы потоков. Доступна и возможность рассмотрения общего отчёта для этого нужно кликнуть в верхнем меню View->Snapshot Overview или использовать комбинацию Ctrl+Shift+O.

Уставший сборщик мусора

Что же мы смогли выяснить благодаря dotTrace? Ну, во-первых, мы в очередной раз убедились, что C#-анализатор не использует процессорные мощности даже наполовину. PVS-Studio C# многопоточное приложение, и, по идее, нагрузка на процессор должна быть ощутимой. Несмотря на это, при анализе загрузка процессора часто падала до 1315% общей мощности CPU. Очевидно, работаем неэффективно, но почему?

dotTrace показал нам, что большую часть времени анализа работает даже не само приложение, а сборщик мусора! Возникает логичный вопрос как же так?

Дело в том, что запуск сборки блокировал потоки анализатора. Сборка завершалась, анализатор немного поработал и снова запускается сборка мусора, а PVS-Studio "отдыхает".

Осознав проблему, мы поняли, что нужно отыскать места, в которых выделение памяти под новые объекты производится наиболее активно. Затем нужно было проанализировать найденные фрагменты и внести необходимые для оптимизации изменения.

Мы не виноваты, это всё их DisplayPart!

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

Возможно, мы могли бы вообще отказаться от использования этих объектов, если бы не один нюанс. В исходниках нашего C#-анализатора DisplayPart даже не упоминается! Как оказалось, этот тип играет определённую роль в используемом нами Roslyn API.

Roslyn (или .NET Compiler Platform) является основой C#-анализатора PVS-Studio. Он предоставляет нам готовые решения для ряда задач:

  • преобразование файла с исходным кодом в синтаксическое дерево;

  • удобный способ обхода синтаксического дерева;

  • получение различной (в том числе семантической) информации о конкретном узле дерева;

  • и т.д.

Roslyn платформа с открытым исходным кодом. Это позволило без проблем понять, что такое *DisplayPart *и зачем этот тип вообще нужен.

Оказалось, что объекты DisplayPart активно используются при создании строковых представлений так называемых символов. Если не погружаться в детали, то символ это объект, содержащий семантическую информацию о некоторой сущности в исходном коде. К примеру, символ метода позволяет получить данные о параметрах данного метода, классе-родителе, возвращаемом типе и т.д. Более подробно данная тема освещена в статье "Введение в Roslyn. Использование для разработки инструментов статического анализа". Очень рекомендую к прочтению всем, кто интересуется статическим анализом (вне зависимости от предпочитаемого языка программирования).

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

Как это обычно и бывает, локализация проблемы = 90% её решения. Раз уж вызовы ToString у символов создают столько проблем, то, может, и не стоит производить их?

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

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

Описанная правка, вопреки ожиданиям, практически не увеличила загрузку процессора (изменение составляло буквально несколько процентов). Тем не менее, PVS-Studio стал работать значительно быстрее: один из наших тестовых проектов ранее анализировался 2,5 часа, а после правок анализ проходил всего за 2. Ускорение работы на 20% действительно радовало.

Упакованный Enumerator

На втором месте по количеству выделяемой памяти были объекты типа List<T>.Enumerator, используемые при обходе соответствующих коллекций. Итератор списка является структурой, а значит, создаётся на стеке. Тем не менее, трассировка показывала, что такие объекты в больших количествах попадали в кучу! С этим нужно было разобраться.

Объект значимого типа может попасть в кучу в результате упаковки (boxing). Она выполняется при приведении объекта значимого типа к object или реализуемому интерфейсу. Итератор списка реализует интерфейс IEnumerator, и именно приведение к этому интерфейсу вело к попаданию итератора в кучу.

Для получения объекта Enumerator используется метод GetEnumerator. Общеизвестно, что это метод, определённый в интерфейсе IEnumerable. Взглянув на его сигнатуру, можно заметить, что возвращаемый тип данного метода IEnumerator. Получается, что вызов GetEnumerator у списка всегда приводит к упаковке?

А вот и нет! Метод GetEnumerator, определённый в классе List, возвращает структуру:

Так всё-таки будет упаковка производиться или нет? Ответ на этот вопрос зависит от типа ссылки, у которой вызывается GetEnumerator:

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

Конечно, разница невелика, если такой *Enumerator *создаётся лишь пару сотен раз за время работы программы. Однако при анализе более-менее объёмного проекта в нашем C#-анализаторе эти объекты создаются миллионы или даже десятки миллионов раз. В таких случаях разница становится весьма ощутимой.

Примечание. Как правило, мы не вызываем GetEnumerator напрямую. Зато достаточно часто приходится использовать цикл foreach. Именно он "под капотом" получает итератор. Если в foreach передана ссылка типа List, то и итератор, используемый в foreach, будет лежать на стеке. Если же с помощью foreach производится обход абстрактного IEnumerable, то итератор будет сохранён в куче, а foreach будет работать со ссылкой типа IEnumerator. Описанное поведение актуально и для других коллекций, в которых присутствует GetEnumerator, возвращающий итератор значимого типа.

Конечно, нельзя полностью отказаться от использования IEnumerable. Однако в коде анализатора мы обнаружили множество мест, где метод принимал в качестве аргумента абстрактный IEnumerable, а при его вызове всегда передавался вполне конкретный List.

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

И ты, LINQ?!

Методы расширения, определённые в пространстве имён System.Linq, используются для работы с коллекциями повсеместно. Достаточно часто они действительно позволяют упростить код. Наверное, ни один более-менее серьёзный проект не обходится без использования всеми любимых методов Where, Select и т. д. C#-анализатор PVS-Studio не исключение.

Что ж, красота и удобство LINQ-методов дорого нам обошлись. Так дорого, что во многих местах мы отказались от их использования в пользу простого foreach. Как же так вышло?

Основная проблема снова состояла в создании огромного количества объектов, реализующих интерфейс IEnumerator. Такие объекты создаются на каждый вызов LINQ-метода. Взгляните на следующий код:

List<int> sourceList = ....var enumeration = sourceList.Where(item => item > 0)                            .Select(item => someArray[item])                            .Where(item => item > 0)                            .Take(5);

Сколько итераторов будет создано при его выполнении? Давайте посчитаем! Чтобы понять, как всё это работает, откроем исходники System.Linq. Они доступны на github по ссылке.

При вызове Where будет создан объект класса WhereListIterator особая версия Where-итератора, оптимизированная для работы с List (похожая оптимизация есть и для массивов). Данный итератор хранит внутри ссылку на список. При переборе коллекции WhereListIterator сохранит в себе итератор списка, после чего будет использовать его при работе. Так как WhereListIterator рассчитан именно на список, то приведение итератора к типу IEnumerator не производится. Однако сам *WhereListIterator *является классом, а значит, его экземпляры попадут в кучу. Следовательно, исходный итератор в любом случае будет храниться не на стеке.

Вызов Select приведёт к созданию объекта класса WhereSelectListIterator. Очевидно, и он будет храниться в куче.

Последующие вызовы Where и *Take *также приведут к созданию итераторов и выделению памяти под них.

Итого в куче будет выделена память под 5 итераторов. Позже её придётся освобождать, что станет дополнительной работой для сборщика мусора.

Теперь взглянем на фрагмент, написанный с использованием foreach:

List<int> sourceList = ....List<int> result = new List<int>();foreach (var item in sourceList){  if (item > 0)  {    var arrayItem = someArray[item];    if (arrayItem > 0)    {      result.Add(arrayItem);      if (result.Count == 5)        break;    }  }}

Давайте попробуем проанализировать сравнить подходы с foreach и LINQ.

  • Преимущества варианта с LINQ-вызовами:

    • короче, приятнее выглядит и в целом лучше читается;

    • не требует создания коллекции для хранения результата;

    • вычисление значений будет произведено только при обращении к элементам;

    • в большинстве случаев объект, полученный в результате запроса, хранит только один элемент последовательности.

  • Недостатки варианта с LINQ-вызовами:

    • память в куче выделяется гораздо чаще: в первом примере туда попадает 5 объектов, а во втором только 1 (список result);

    • повторные обходы последовательности приводят к повторному выполнению обхода с выполнением всех указанных функций. Случаи, когда такое поведение действительно полезно, довольно редки. Конечно, можно использовать методы типа ToList, однако это сводит преимущества варианта с LINQ-вызовами на нет (кроме первого).

В целом недостатки не очень весомы, если LINQ-запрос выполняется относительно нечасто. Однако мы оказались в ситуации, когда это происходило сотни тысяч и даже миллионы раз. Кроме того, важно понимать, что эти запросы были вовсе не так просты, как в приведённом примере.

При всём этом мы заметили, что в подавляющем большинстве случаев у нас совершенно не было интереса в отложенном выполнении. Либо для результата LINQ-операций сразу вызывался какой-нибудь ToList, либо код запросов выполнялся по несколько раз при повторных обходах коллекции (что не есть хорошо).

*Замечание. *На самом деле существует простой способ реализовать отложенное выполнение и не плодить при этом лишние итераторы. Возможно, вы догадались, что я говорю о ключевом слове yield. С его помощью можно реализовывать генерацию последовательности элементов, задавать любые правила и условия добавления элементов в последовательность. Подробнее о возможностях yield в C# (а также о том, как эта штука работает внутри) можно найти в статье "Что такое yield и как он работает в C#?".

Изучив внимательно код анализатора, мы обнаружили множество мест, в которых оптимальнее использовать foreach вместо LINQ-методов. Это позволило существенно сократить количество необходимых операций выделения памяти в куче и сборки мусора.

Что же в итоге?

Успех!

Оптимизация работы PVS-Studio прошла успешно! Мы добились успехов в уменьшении потребляемой памяти, а также серьёзно увеличили скорость анализа (на некоторых проектах скорость работы увеличилась более чем на 20%, а пиковое потребление памяти сократилось практически на 70%!). А ведь всё начиналось с непонятной истории клиента о том, как он три дня не мог проверить свой проект! Тем не менее, на этом оптимизация работы анализатора не заканчивается, и мы продолжаем находить новые способы совершенствования PVS-Studio.

Изучение проблем заняло у нас куда больше времени, чем их решение. Но рассказанная история произошла очень давно. Сейчас, как правило, подобные вопросы решаются командой PVS-Studio куда быстрее. Главными помощниками в исследовании проблем выступают различные инструменты, такие как трассировщик и профилировщик. В этой статье я рассказывал о нашем опыте работы с dotMemory и dotPeek, однако это вовсе не означает, что эти приложения единственные в своём роде. Пожалуйста, напишите в комментариях, какими инструментами в таких случаях пользуетесь вы.

Это ещё не конец

Да, мы действительно решили проблему клиента и даже ускорили анализатор в целом, однако... Очевидно, он работает далеко не так быстро, как мог бы. PVS-Studio всё ещё недостаточно активно использует процессорные мощности. Проблема состоит не совсем в алгоритмах анализа проверка каждого файла в отдельном потоке позволяет обеспечивать достаточно высокий уровень параллелизма. Основной бедой производительности C#-анализатора является сборщик мусора, который очень часто блокирует работу всех потоков, что сильно замедляет PVS-Studio. Даже если анализатор будет использовать сотни ядер, скорость работы будет снижаться из-за частой блокировки потоков сборщиком, который, в свою очередь, не может использовать все доступные мощности при выполнении своих задач в силу некоторых алгоритмических ограничений.

Однако и это не тупик, а лишь ещё одно препятствие, которое мы должны преодолеть. Некоторое время назад до меня дошла "секретная информация" о планах реализации процесса анализа... в нескольких процессах! Это позволит обойти существующие ограничения, ведь сборка мусора в одном из процессов не будет влиять на анализ, выполняющийся в других. Подобный подход позволит эффективно использовать большое количество ядер в том числе и с использованием Incredibuild. Кстати, похожим образом уже работает C++ анализатор, для которого давным-давно доступна возможность распределённого анализа.

Откуда ещё берутся проблемы с производительностью

Кстати, стоит сказать, что проблемы с производительностью часто бывают связаны не с чрезмерным использованием LINQ-запросов или чем-то подобным, а с самыми обыкновенными ошибками в коде. Какие-нибудь "always true"-условия, заставляющие метод работать гораздо дольше, чем необходимо, опечатки и прочее всё это может негативно сказаться как на производительности, так и на корректности работы приложения в целом.

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

PVS-Studio один из таких анализаторов. Он использует серьёзные технологии вроде межпроцедурного анализа или анализа потока данных, что позволяет значительно повысить надёжность кода любого приложения. Кроме того, одним из наиболее приоритетных направлений работы компании является поддержка пользователей, решение их вопросов и возникающих проблем, а в некоторых случаях мы даже добавляем по просьбе клиента новый функционал :). Смело пишите нам по всем возникающим вопросам! А чтобы попробовать анализатор в деле вы можете перейти по ссылке. Удачного использования!

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Nikita Lipilin. .NET Application Optimization: Simple Edits Speeded Up PVS-Studio and Reduced Memory Consumption by 70%.

Подробнее..

Шардинг, от которого невозможно отказаться

22.04.2021 10:12:30 | Автор: admin
image

А не пора ли нам шардить коллекции?
Не-е-е:


  • у нас нет времени, мы пилим фичи!
  • CPU занят всего на 80% на 64 ядерной виртуалке!
  • данных всего 2Tb!
  • наш ежедневный бекап идет как раз 24 часа!

В принципе, для большинства проектов вcё оправдано. Это может быть еще прототип или круг пользователей ограничен Да и не факт, что проект вообще выстрелит.
Откладывать можно сколько угодно, но если проект не просто жив, а еще и растет, то до шардинга он доберется. Одна беда, обычно, бизнес логика не готова к таким "внезапным" вызовам.
А вы закладывали возможность шардинга при проектировании коллекций?


Эта статья для продвинутых разработчиков.
Для тех, кто планирует шардинг своего кластера.
Для тех, кто уже шардировал кластер ранее, но админы все еще плачут.
Для тех, кто руками перемещал jumbo-чанки.


Сначала, мы будем учится жить со слонами.
Потом, мы их победим, но не сможем вернуться назад.


Всем привет, от команды разработки Smartcat и наших счастливых админов!


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


Зачем нам шардинг


Шардинг штатная возможность горизонтального масштабирования в MongoDB. Но, чтобы стоимость нашего шардинга была линейной, нам надо чтобы балансировщик MongoDB мог:


  • выровнять размер занимаемых данных на шард. Это сумма размера индекса и сжатых данных на диске.
  • выровнять нагрузку по CPU. Его расходуют: поиск по индексу, чтение, запись и агрегации.
  • выровнять размер по update-трафику. Его объем определяет скорость ротации oplog. Это время за которое мы можем: поднять упавший сервер, подключить новую реплику, снять дамп данных.

Конечно, мало толку будет от нового шарда, если на него переместится только пара процентов данных или не дойдет нагрузка на CPU или update-трафик.


Особенности шардинга в MongoDB


Как мы знаем балансировщик MongoDB при отсутствии ограничений (превышение размера данных на шард, нарушение границ зон) выравнивает только количество чанков на шард. Это простой, надежный и сходящийся алгоритм.
Есть документация про лимиты количества чанков и про порядок выбора размещения чанка.
Этот алгоритм и определяет главные ограничения на балансируемые данные:


  1. Ключ шардирования должен быть высокоселективный. В противном случае мы не получим достаточного числа интервалов данных для балансировки.
  2. Данные должны поступать с равномерным распределением на весь интервал значений ключа. Тривиальный пример неудачного ключа это возрастающий int или ObjectId. Все операции по вставке данных будут маршрутизироваться на последний шард (maxKey в качестве верхней границы).

Самое значимое ограничение гранулярность ключа шардирования.
Или, если сформулировать отталкиваясь от данных, на одно значение ключа должно приходиться мало данных. Где "мало" это предельный размер чанка (от 1Mb до 1Gb) и количество документов не превышает вот эту вот величину.


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


Бизнес логика требует слонов


Теперь давайте посмотрим, с чем мы будем сталкиваться при проектировании бизнес логики.
Я рассмотрю только самый чувствительный для шардирования случай, который заставляет наши данные группироваться в неравномерные объемы.
Рассмотрим следующий сценарий:


  • мы шардируем коллекцию работ
  • работа принадлежит только одному проекту
  • идентификатор проекта равномерно распределен (а если это не так, то помогает хешированный индекс)
  • большая часть поисковых запросов знает только идентификатор проекта
  • число работ на проект не лимитировано. Могут быть проекты с одной работой, а могут быть и гиганты с миллионом работ.

Пример модели:


{    _id: ObjectId("507c7f79bcf86cd7994f6c0e"),    projectId: UUID("3b241101-e2bb-4255-8caf-4136c566a962"),    name: "job name 1",    creation: ISODate("2016-05-18T16:00:00Z"),    payload: "any additional info"}

Как может выглядеть логика выбора ключа шардирования?
Совершенно точно, в качестве первого поля стоит взять projectId. Он есть в большинстве запросов, т.е. бОльшая часть запросов будет направлена роутером на нужный шард.


Теперь, надо выбрать второе поле ключа или оставить только первое.
Например, у нас 20% запросов используют только поле name, еще 20% только поле creation, а остальные опираются на другие поля.
Если в ключ шардирования включить второе поле, то крупные проекты, те у которых объем работ не помещается в одном чанке, будут разделены на несколько чанков. В процессе разделения, высока вероятность, что новый чанк будет отправлен на другой шард и для сбора результатов запроса нам придётся обращаться к нескольким серверам. Если мы выберем name, то до 80% запросов будут выполняться на нескольких шардах, тоже самое с полем creation. Если до шардирования запрос выполнялся на одном сервере, а после шардирования на нескольких, то нам придется дополнительную читающую нагрузку компенсировать дополнительными репликами, что увеличивает стоимость масштабирования.


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


  • у нас могут появляться не перемещаемые jumbo-чанки. При большом объеме работ в одном проекте мы обязательно превысим предельный размер чанка. Эти чанки не будут разделены балансировщиком.
  • у нас будут появляться пустые чанки. При увеличении проекта, балансировщик будет уменьшать диапазон данных чанка для этого проекта. В пределе, там останется только один идентификатор. Когда большой проект будет удален, то на этот узкий чанк данные больше не попадут.

Оба этих случая несколько искажают KPI штатного балансировщика. В первом случае чанк вообще не перемещается. Во втором, он перемещается, но это не приводит к смещению данных. С течением времени, мы можем получить ситуацию когда число пустых чанков превышает число чанков с данными, а еще часть чанков не перемещаемые.


В общем, мы можем посмотреть текущее распределение данных или просто прикинуть развитие проекта и допустить, что хоть jumbo-чанки и будут появляться, но их должно быть не много.


Проблемы с масштабированием


Итак, мы, вопреки ожиданиям, набрали достаточно неперемещаемых чанков, чтобы это стало заметно. То есть буквально, случай из практики. Вы заказываете админам новый шард за X$ в месяц. По логам видим равномерное распределение чанков, но занимаемое место на диске не превышает половины. С одной стороны весьма странное расходование средств было бы, а с другой стороны возникает вопрос: мы что же, не можем прогнозировать совершенно рутинную операцию по добавлению шарда? Нам совсем не нужно участие разработчика или DBA в этот момент.


Что интересно, на этапе проектирования схемы данных, у нас и нет особого выбора. Мы не можем предсказать характер использования на годы вперед. А владельцы продукта очень не охотно формулируют строгие ограничения при проектировании. Даже оглядываясь назад, не всегда понятно, как можно было бы перепроектироваться, чтобы успеть все сделать тогда. Так что продолжаем работать с тем шардированием, которое заложили.


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


Надеюсь, тут уже все уже запуганы и потеряли надежду. ;)


Решение


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


1-й воспользоваться командой moveChunk прямое указание балансировщику о перемещении конкретного чанка.


2-й воспользоваться командой addTagRange привязка диапазона значений ключа шардирования к некоторому шарду и их группе.


В обоих случаях потребуется также точное знание размера чанка. Это можно сделать командой dataSize, которая возвращает некоторую статистику по объектам в коллекции на заданном интервале ключей.


Предварительное прототипирование 1-го варианта выявило дополнительные особенности.


Команды moveChunk не отменяют работы штатного балансировщика. Он вполне может принять решение об обратном переносе. Тут надо блокировать работу штатного балансировщика. Или кроме перемещения больших чанков на недогруженный шард, надо искать на нем маленькие чанки и синхронно перемещать их на перегруженный шард.


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


Массовые сканирования dataSize ухудшают отзывчивость сервера на боевых запросах.


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


По совокупности проблем с прямой балансировкой, я решил развивать балансировку через привязку диапазона значений ключа шардирования.


Основная идея проста.
Давайте подберем такие диапазоны ключей на каждам шарде, чтобы в итоге части коллекции занимали одинаковый размер на шардах.


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


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


Группировка данных


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


Итак, коллекция уже шардирована.


  • Читаем все ее чанки из коллекции config.chunks с сортировкой по возрастанию ключа {min: 1}
  • Распределяем чанки по шардам, так чтобы их было примерно одинаковое количество. Но при этом все чанки на одном шарде должны объединяться в один интервал.

Например:


У нас есть три шарда sh0, sh1, sh2 с одноименными тегами.
Мы вычитали поток из 100 чанков по возрастанию в массив


var chunks = db.chunks.find({ ns: "demo.coll"}).sort({ min: 1}).toArray();

Первые 34 чанка будем размещать на sh0
Следующие 33 чанка разместим на sh1
Последние 33 чанка разместим на sh2
У каждого чанка есть поля min и max. По этим полям мы выставим границы.


sh.addTagRange( "demo.coll", {shField: chunks[0].min}, {shField: chunks[33].max}, "sh0");sh.addTagRange( "demo.coll", {shField: chunks[34].min}, {shField: chunks[66].max}, "sh1");sh.addTagRange( "demo.coll", {shField: chunks[67].min}, {shField: chunks[99].max}, "sh2");

Обратите внимание, что поле max совпадает с полем min следующего чанка. А граничные значения, т.е. chunks[0].min и chunks[99].max, всегда будут равны MinKey и MaxKey соответственно.
Т.е. мы покрываем этими зонами все значения ключа шардирования.


Балансировщик начнёт перемещать чанки в указанные диапазоны.
А мы просто ждем окончания работы балансировщика. Т.е. когда все чанки займут свое место назначения. Ну за исключением jumbo-чанков конечно.


Коррекция размера


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


sh.addTagRange( "demo.coll", {shField: MinKey}, {shField: 1025}, "sh0");sh.addTagRange( "demo.coll", {shField: 1025}, {shField: 5089}, "sh1");sh.addTagRange( "demo.coll", {shField: 5089}, {shField: MaxKey}, "sh2");

Вот так будет выглядеть размещение чанков:


Командой db.demo.coll.stats() можно получить объем данных, которые хранятся на каждом шарде. По всем шардам можно вычислить среднее значение, к которому мы хотели бы привести каждый шард.


Если шард надо увеличить, то его границы надо перемещать наружу, если уменьшить, то внутрь.
Так как уже явно заданы диапазоны ключей, то балансировщик не будет их перемещать с шарда на шард. Следовательно, мы можем пользоваться командой dataSize, мы точно знаем данные какого шарда мы сканируем.
Например, нам надо увеличить sh0 за счет h1. Границу с ключем будем двигать в бОльшую сторону. Сколько именно данных мы сместим перемещением конкретного 34-го чанка, мы можем узнать командой dataSize с границами этого чанка.


db.runCommand({ dataSize: "demo.coll", keyPattern: { shField: 1 }, min: { shField: 1025 }, max: { shField: 1508 } })

Последовательно сканируя чанки по одному мы мы можем смещать границу до нужного нам размера sh0.


Вот так будет выглядеть смещение границы на один чанк



Вот пример команд для фиксации нового состояния границ в конфигурации кластера.
Мы сначала удаляем пару интервалов со старыми значениями границ, потом заводим новые интервалы с актуальными значениями границ.


sh.removeTagRange( "demo.coll", {shField: MinKey}, {shField: 1025}, "sh0");sh.removeTagRange( "demo.coll", {shField: 1025}, {shField: 5089}, "sh1");sh.addTagRange( "demo.coll", {shField: MinKey}, {shField: 1508}, "sh0");sh.addTagRange( "demo.coll", {shField: 1508}, {shField: 5089}, "sh1");

После применения команд балансировщик самостоятельно переместит 34-й чанк на нужный нам шард. Конечно, нет необходимости менять границы после каждого сканирования чанка. Мы можем учесть полученный эффект, продолжить сканирование и сформировать уже итоговый пакет изменений границ. Таким образом, мы можем выровнять размер коллекции по шардам с точностью до максимального размера чанка.
Конечно, в процессе сканирования нам будут попадаться jumbo-чанки. Но ничего страшного, мы их просто игнорируем и переносим границу через него без учета эффекта от перемещения.
В общем случае, мы можем планировать произвольное распределений данных таких коллекций на шардах кластера.


Выгодные особенности этого подхода:


  • до смещения границ, данные не перемещаются. Мы можем их сканировать без опасения, что эта информация устареет. Ну конечно, исключая работу приложения для которого и заведена эта БД.
  • команда dataSize применяется только к перемещаемым данным. Мы не сканируем лишнего.
  • путь к целевому состоянию непрерывный. Т.е. мы можем проводить частичные коррекции, например если видим, что сканирование идет долго и реальное распределение данных успевает сильно измениться.

Дополнительные возможности


Вообще, на практике требуется выравнивание используемого объема диска на шардах, а не только части шардированных коллекции. Частенько, нет времени или возможности проектировать шардирование вообще всех БД и коллекций. Эти данных лежат на своих primary-shard. Если их объем мал, то его легко учесть при коррекции размера и просто часть данных оттащить на другие шарды.


С течением времени, особенно после частых удалений, будут образовываться пустые чанки. Не то чтобы они теперь сильно мешают, но их может быть сильно больше чанков с данными. Чтобы они не заставляли страдать роутер, было бы хорошо их прибрать. Раньше (до дефрагментации) их надо было найти, переместить к другому чанку-соседу на один шард, потом запустить команду mergeChunks. Перемещение нужно, т.к. команда объединяет только соседние чанки на одном шарде.
Есть даже скрипты для автоматизации, но они долго работают, и есть один тикет с печально низким приоритетом.


Теперь все значительно проще. Соседние чанки и так на одном шарде. Можно объединять хоть весь интервал целиком. Если он конечно без jumbo-чанков, их надо исключить. Есть на интервале пустые чанки или нет, это уже не важно. Балансировщик заново сделает разбиение по мере добавления или изменения данных.


Почти итог


Итак, проблема решена.
Хорошо, что не пришлось заменять или сильно вмешиваться в работу штатного балансировщика. Он продолжает рулить другими коллекциями или диапазонами данных. А в наших настроенных диапазонах он выполняет разделение чанков и таскает чанки между серверами как мы попросим.


Наши плюсы:


  • точная балансировка занимаемого размера
  • лучшее распределение update-трафика
  • минимальное сканирование при коррекциях
  • бесплатная уборка пустых чанков
  • минимальные допуски на запасное/неиспользуемое дисковое пространство на каждом шарде
  • мы всегда можем удалить привязки диапазонов ключей и вернуться к исходному состоянию

Минусы:


  • требуется настройка и сопровождение привязок диапазонов ключей.
  • усложняется процесс добавления нового шарда.

Но это было бы слишком скучно Время победить слонов и не вернуться!


Победа над слонами!


Как мы уже отмечали, единственная причина неперемещаемых чанков недостаточная селективность ключа шардирования. Мы сами снижали селективность ключа для привязки запроса к шарду. Но мы уже сгруппировали данные, а теперь мы можем этим коварно воспользоваться!


Можно увеличить селективность ключа шардирования, если добавить к нему второе поле с достаточной селективностью. Поле может быть любым. Подойдет даже _id.


Вспомним пример модели:


{    _id: ObjectId("507c7f79bcf86cd7994f6c0e"),    projectId: UUID("3b241101-e2bb-4255-8caf-4136c566a962"),    name: "job name 1",    creation: ISODate("2016-05-18T16:00:00Z"),    payload: "any additional info"}

Ранее мы выбрали ключ шардирования {projectId: 1}
Но теперь при проектировании можно выбрать любые уточняющие поля для ключа шардирования:


  • {projectId: 1, name: 1}
  • {projectId: 1, creation: 1}
  • {projectId: 1, _id: 1}

А если вы пользуетесь MongoDB версии 4.4 и выше, то там есть удобная функция уточнение ключа шардирования на существующих данных.


Данные дефрагментированы, а это дает нам гарантии того, что основной объем документов работ с одинаковым projectId будет находиться на одном шарде. Как это выглядит на практике?


Вот иллюстрация примеров размещения работ по чанкам. В случае, если мы выбрали ключ шардирования {projectId: 1, _id: 1}



Здесь, для упрощения примера, идентификаторы представлены целыми числами.
А термином "проект" я буду называть группу работ с одинаковым projectId.


Некоторые проекты будут полностью умещаться в один чанк. Например, проекты 1 и 2 размещены в 1м чанке, а 7-й проект во 2-м.
Некоторые проекты будут размещены в нескольких чанках, но это будут чанки с соседними границами. Например, проект 10 размещен в 3, 4 и 5 чанках, а проект 18 в 6 и 7 чанках.
Если мы будем искать работу по ее полю projectId, но без _id, то как будет выглядеть роутинг запросов?


Планировщик запросов MongoDB отлично справляется с исключением из плана запроса тех шардов, на которых точно нет нужных данных.
Например, поиск по условию {projectId: 10, name: "job1"} будет только на шарде sh0


А если проект разбит границей шарда? Вот как 18-й проект например. Его 6-й чанк находится на шарде sh0, а 7-й чанк находится на шарде sh1.
В этом случае поиск по условию {projectId: 18, name: "job1"} будет только на 2х шардах sh0 и sh1. Если известно, что размер проектов у нас меньше размера шарда, то поиск будет ограничен только этими 2-мя шардами.


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


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


  • группа располагается на одном, максимум двух шардах.
  • число групп которые имели несчастье разместиться на 2х шардах ограничено числом границ. Для N шардов будет N-1 граница.

Больше одно шардовых запросов, меньше лишней нагрузки на чтение, меньше дополнительных реплик!
Мало нам было бы радости, но с таких сильно селективным ключом мы и от jumbo-чанков избавляемся.


И вот теперь, от дефрагментации данных нам уже никуда не деться.


Точно итог


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


Теперь уже можно оценить достижения и потери.
Достижения:


  • гарантированный способ выравнивания занимаемого места для данных, которые мы хоть как нибудь смогли расшардить.
  • простая компенсация размера не шардированных коллекций. Сложно представить себе проект, в котором найдется время проектировать каждую коллекцию с учетом шардирования.
  • больше никаких jumbo-чанков.
  • больше никаких пустых чанков. Соседние чанки на одном шарде это слияние всего в одну в одну команду всего диапазона.

Потери:


  • если за счет дефрагментации мы убирали jumbo-чанки, то мы теперь с ней навсегда. Вернуть ключ мы не можем. Удаление границ приведет к фрагментации данных и снижению доли одно шардовых запросов.
  • периодически надо проводить коррекции границ. Но кажется это мы сможем автоматизировать.
  • при добавлении нового шарда надо явно планировать переразбиение.

Осталось спроектировать весь процесс дефрагментации, расчета поправок и коррекции границ Ждите!

Подробнее..

JuliaR преимущества интеграции

24.03.2021 08:16:47 | Автор: admin

R стабильный, удобный, имеет прекрасную экосистему. Один из признанных лидеров в области анализа и визуализации данных и, наверное, лучший инструмент для статистических исследований. Julia тоже удобна, весьма стройна и при этом ещё и быстра. Она ещё очень молода, не имеет такого количества пакетов и пользователей, но скорость работы впечатляет. Особенно по сравнению с R. Оба языка активно используются для анализа данных и машинного обучения. Что же выбрать?

Сопоставление производительности набора программ, реализованных на разных языках программирования, относительно производительности их реализаций на языке Chttps://julialang.org/benchmarks/Сопоставление производительности набора программ, реализованных на разных языках программирования, относительно производительности их реализаций на языке Chttps://julialang.org/benchmarks/

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

Зачем вообще что-то интегрировать? Почему нельзя просто использовать R (Julia)?

Кратко рассмотрим преимущества обоих языков.

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

Julia тоже удобна, тоже имеет приятный синтаксис, немного более строга к пользователю, но имеет свою особенную стройность. Из уникальных особенностей стоит отметить концепцию множественной диспетчеризации, возможность лёгкого доступа к генерируемому низкоуровневому коду, вплоть до ассемблерного, удобную систему макросов. Если бы этим качества и ограничивались, то она была бы просто аналогом R, но Julia при этом очень производительна, даже по меркам компилируемых языков. Вместе с С, C++ и Fortran Julia входит в престижный клуб языков с петафлопсной производительностью. Код на Julia можно запускать как на обычных многоядерных CPU, так и на GPU или вычислительных кластерах. Язык активно используется при проведении научных симуляций, обработке больших массивов данных, задачах машинного обучения. По сравнению с R аналогичные программы на Julia почти всегда в десятки-сотни раз быстрее. При этом благодаря JIT-компиляции команды выполняются почти на лету, как правило без серьёзных задержек. Не так молниеносно, как в R, задержки иногда всё же возникают, Julia как бы берёт некоторую паузу на подумать, особенно когда по ходу дела вдруг надо скомпилировать какой-нибудь пакет. Но подумав и перейдя к вычислениям, Julia работает с очень высокой производительностью. А существенные задержки, во-первых, случаются не так часто и поэтому не слишком раздражают, и к тому же при желании их можно максимально сократить, а во-вторых, ведётся работа над тем, чтобы повысить отзывчивость исполнения команд. Из-за сочетания производительности и удобства использования популярность Julia стремительно набирает обороты. В рейтингах TIOBE и PYPL язык уверенно движется к двадцатке лидеров и имеет вполне реальные шансы войти в неё уже в этом году. С августа 2018 года, когда вышла версия 1.0 и язык в значительной степени стабилизировался, прошло уже достаточно времени, чтобы он перестал ассоциироваться с чем-то сырым и постоянно меняющимся, появилась какая-то литература по актуальной версии. С другой стороны, многие рецепты из сети просто не работают, так как были опубликованы во времена старых версий и язык с тех пор уже претерпел многие изменения. Пока что по современным версиям языка очень мало литературы (на русском по версиям 1.0 и выше печатных изданий пока что вообще нет), нет такой прозрачной системы документации, как у R, далеко не для всех проблем можно найти в сети готовые красивые решения. С Julia постоянно чувствуешь себя первооткрывателем. Но это тоже приятное ощущение.

Сопоставление популярности языков R и Julia по данным рейтинга PYPLhttps://pypl.github.io/PYPL.htmlСопоставление популярности языков R и Julia по данным рейтинга PYPLhttps://pypl.github.io/PYPL.html

Что может дать использование Julia пользователям R

Julia явно быстрее, поэтому на ней разумно производить все ресурсоёмкие вычисления. Есть рецепты, как делать в R вставки на Fortran или C++, но по сравнению с ними Julia имеет гораздо более близкий к R синтаксис, вплоть до прямого заимствования отдельных команд из R. При этом по производительности она находится где-то на уровне близком к C и как правило обгоняет Fortran. В отличие от Fortran, С и C++ код в Julia компилируется на лету. Это позволяет сохранить высокую интерактивность работы, привычную после опыта в R. Вплоть до того, что в Atom и VS Code наиболее популярных средах разработки для Julia, можно точно так же, как в RStudio (наиболее популярной среде разработки для R) отправлять на исполнение произвольные куски кода по Ctrl+Enter. То есть общий подход к работе и ощущение от работы остаются примерно такими же, как и в R, но становится доступна очень высокая производительность.

У Julia есть потенциал, чтобы стать универсальным языком учёных будущего, каким раньше был Fortran. Похоже, ставка авторов Julia на сочетание скорости, удобства и открытости вполне оправдалась. Современный R несмотря на свою приятность сильно ограничен максимально возможной производительностью. Ради оптимизации под производительность приходится отказываться от циклов и писать все ресурсоёмкие процедуры в векторизованном стиле, из-за чего код становится тяжёлым для чтения и отладки. Но даже в максимально оптимизированном виде производительность далеко отстаёт от компилируемых языков. Ведётся работа над повышением производительности, но результат как правило нужен уже сейчас, поэтому приходится искать инструменты, которые прямо сейчас позволяют достичь его. Julia позволяет полноценно использовать циклы, предлагает простые средства для организации параллельных вычислений. Интересной особенностью экосистемы Julia являются интерактивные блокноты Pluto, позволяющие создавать красиво свёрстанные интерактивные научные презентации c формулами в LaTeX и выполняющимися на лету компьютерными симуляциями, меняющими своё поведение в зависимости от изменяемых в ходе показа параметров. Наверное, как-то так должны выглядеть научные публикации будущего.

Допустим, всё это звучит убедительно. Но выбрасывать все свои наработки на R ради освоения какого-то нового языка как-то не хочется. Хорошая новость в том, что если вместо перехода на Julia интегрироваться с ней, то можно ничего не выбрасывать, а всё так же, как и раньше, использовать все свои программы, шаблоны графики, подключаемые библиотеки. Всё это будет работать, как и раньше, и не потребует никаких модификаций.

Что может дать использование R пользователям Julia

Даже если Julia это универсальный язык учёных будущего, жить и работать учёным всё же приходится в настоящем. И в настоящем стоит отметить очень юный возраст Julia. Несмотря на то, что к настоящему времени экосистема языка и основных пакетов более-менее стабилизировалась, такого багажа наработок, который есть для R, удастся достичь ещё нескоро. R полезен хотя бы тем, что он используется как основной язык для демонстрации излагаемых концепций в учебных курсах и специализированной литературе, посвящённой решению конкретных прикладных задач. Есть прекрасные издания по анализу и визуализации данных, статистическому обучению, картографии и другим практическим областям, иллюстрированные примерами на R, в том числе - с использованием уникальных для R инструментов. В некоторых случаях подобные издания могут давать уникальную возможность получения знаний непосредственно из первых рук, от авторов предлагаемых методов. Можно подождать несколько лет, пока что-то подобное появится и для Julia, но при совместном использовании можно и не ждать, а использовать всё прямо сейчас.

Благодаря своим пакетам R давно уже превратился в своеобразный швейцарский нож, современная функциональность которого просто поражает. Вряд ли Julia в ближайшие годы сможет догнать R по количеству и качеству доступных пакетов, особенно в области статистики, анализа пространственных данных, картографии. Но зачем терять время, если все пакеты из R можно использовать уже сейчас? Да, их можно подключить и использовать практически как свои родные. В частности, это даёт возможность сопоставления результатов работы совсем недавно стабилизировавшихся пакетов Julia с результатами работы многократно проверенных пакетов R.

Отдельная тема - геоинформатика. Здесь R уже давно является одним из отраслевых стандартов. В некотором смысле, современный R с соответствующими пакетами можно рассматривать как открытую геоинформационную систему с текстовым интерфейсом, причём достаточно популярную и мощную. Вполне реальна ситуация, когда заходишь на какой-нибудь портал скачать необходимые для работы открытые пространственные данные, а там помимо готовой карты в pdf и самих данных лежит ещё и скрипт на R, который по ним строит правильно оформленную карту. Хотя в среде Julia тоже наблюдается определённое движение в направлении геоинформатики, до уровня R тут пока что ещё далеко. Но, опять же, при желании можно не ждать, а взять всё сразу. Взять всё сразу - это очень в стиле философии Julia.

Что для этого нужно сделать

Очевидно, есть два подхода к решению проблемы: вызывать код R из Julia и вызывать код Julia из R. Для вызова R из Julia есть специальный пакет Rcall. Для вызова Julia из R существует сразу несколько альтернативных решений: пакеты XRJulia, JuliaCall, RJulia и JuliaConnectoR. На данный момент наиболее прогрессивным из них выглядит JuliaConnectoR. Предлагаемый им подход немного тяжеловесен, и ориентирован скорее на использование отдельных функций из Julia, чем на полностью смешанное использование двух языков. Довольно любопытны доводы авторов в польу использования именно R, как связующей среды, опубликованные в их статье на arxiv.org.

Изначально я как раз планировал вызвать Julia из R. Потратил много времени, но так и не смог добиться работоспособности этой конструкции ни с XRJulia, ни с JuliaCall. Какой-либо информации про JuliaConnectoR тогда ещё не было, поэтому не было уверенности, что сама идея совместного использования этих двух языков - здравая. Всё шло к тому, что тяжёлые расчётные части придётся переписать с R на Fortran, с которым соприкасался когда-то в институте, но с тех пор никогда больше не использовал. Просто ради интереса решил попробовать вызвать R из Julia, и каково же было моё удивление, когда всё заработало практически из коробки и оказалось действительно удобным способом совместного использования двух языков. Возможно, это мой субъективный опыт, но именно на этом способе мне хочется сейчас остановиться. Про вызов Julia из R, возможно, расскажу как-нибудь в другой раз.

Итак, что нам понадобится:

1) Установленная Julia

2) Установленный R

3) В Julia необходимо установить пакет Rcall: Pkg.add("RCall")

4) Необходимо прописать путь до R. В Windows это удобнее всего сделать, задав переменную среды. В моём случае: R_HOME = C:\Program Files\R\R-4.0.3

Ну вот, собственно, и всё. Наш волшебный гибрид готов! Теперь в любом месте внутри кода Julia можно вызывать код на R и это будет работать. Можно даже не закрывать RStudio с запущенным сеансом в R. Вызов из Julia будет обрабатываться независимо от него.

Подключаем установленную библиотеку Rcall:

using RCall

Вызываем одиночную команду на R. Если R в результате выполнения команды должен построить график, то график откроется в отдельном окне:

R"plot(rnorm(10))"

Можно последовательно запускать отдельные команды, так, как будто мы работаем в консоли R, и они будут выполняться внутри единого сеанса:

R"var <- 5*8"

R"var"

R"var / 100"

Фактически это и есть сеанс в R, только обёрнутый в команды Julia. На каком языке в данном случае ведётся работа это скорее уже философский вопрос.

Естественно, дальше нам захочется как-то передавать переменные из одного языка в другой. Для этого удобно использовать команды @rput и @rget. Забрасываем какие-то переменные в R, что-то там считаем, забираем результаты обратно:

a = 1

@rput a

R"b <- a*2 + 1"

@rget b

Если необходимо выполнить последовательность команд, например целый лист кода, то удобнее использовать вставку вида:

R"""

"""

Например:

a = 1

@rput a

R"""

b <- a*2 + 1

c <- b*3

"""

@rget c

Внутри вызванного кода можно вызывать любые библиотеки R, и это будет работать так же, как и в обычном сеансе R.

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

Есть ли здесь какие-то подводные камни? Я нашёл пока только два:

В текущей версии не поддерживаются кириллические комментарии в R. Придётся их поудалять или заменить на латинские.

В интерактивных блокнотах Pluto, силе и гордости экосистемы Julia, поддержка R работает странно. При первом запуске всё работает, но при изменении входных данных или содержимого R скрипта вместо обновления порождаемых им объектов происходит разрыв коннекта с R. Я пока что не нашёл способа, как обойти это ограничение.

Ещё небольшой комментарий: для того, чтобы комфортно работать с подобными двуязычными проектами, необходимо, чтобы среда разработки поддерживала оба языка и их совместное использование. Как Atom (прошлый фаворит Джулии, с которым она рассталась летом), так и VS Code (нынешний фаворит) при установке синтаксических дополнений для обоих языков такой режим поддерживают. Но очень может быть, что в каких-то средах такой поддержки не будет. Это может стать проблемой, потому что осуществлять навигацию по простыне кода, оформленного как один огромный комментарий, не очень удобно.

На этом всё! Надеюсь, это небольшая заметка была полезна

Подробнее..

Быстрее, точнее, эффективнее IBM представила прототип 7-нм ИИ-сопроцессора

26.02.2021 20:18:58 | Автор: admin

Для обучения более-менее продвинутых моделей искусственного интеллекта зачастую требуются значительные ресурсы. А сложноустроенные модели подразумевают инфраструктуру в миллионы долларов. IBM создала технологию, позволяющую сократить стоимость и время обучения. И, конечно, сэкономить.

Корпорация представила прототип нового энергоэффективного 7-нм ИИ-сопроцессора. Использование чипа может повысить эффективность обучающих систем более, чем в 7 раз. Беспилотные автомобили, преобразование текста в речь, машинное зрение, разработка новых лекарств, дроны для доставки сферы приложения ИИ сегодня бесконечны. Для реализации этих масштабных проектов и нужны подобные технологии.

В большинстве вычислительных ядер, процессоров и SoC, заточенных под машинное обучение, чаще всего применяют режимы пониженной разрядности: FP16 и INT8. 8-битная точность в этом случае просто избыточна. IBM разработала для подобных периферийных систем прототип нового ИИ-чипа.

Чем интересен сопроцессор?



4-ядерный чип. Источник

Чип спроектирован с использованием 7-нм техпроцесса. Возможны разные сценарии использования чипа:

  • В 4-битном режиме для обычных задач. Технология позволяет добиваться сопоставимой с 8-ми битным режимом точности результатов.
  • В 2-битном режиме для инференс-задач. В этом случае иногда точность понижается на несколько процентов, но в 4 раза повышается производительность по сравнению с 8-битным режимом.
  • Гибридный режим для задач разного типа.

Основные характеристики чипа:

  • 7-нм техпроцесс на основе EUV;
  • использование гибридного формата FP8 с HFP8;
  • управление питанием аппаратных ускорителей с помощью ИИ;
  • адаптация к перманентно высоким нагрузкам, что обеспечивает высокую производительность приложений.


Источник

Сферы применения новинки:

  • обучение крупномасштабных моделей в облаке;
  • усиление безопасности и конфиденциальности;
  • увеличение вычислительных мощностей в гибридных средах без больших затрат энергии.

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

Подробнее..

Бенчмарки VKUI и других ребят из UI-библиотек

26.05.2021 12:10:08 | Автор: admin

Меня зовут Григорий Горбовской, я работаю в Web-команде департамента по экосистемным продуктам ВКонтакте, занимаюсь разработкой VKUI.

Хочу вкратце рассказать, как мы написали 8 тестовых веб-приложений, подключили их к моно-репозиторию, автоматизировали аудит через Google Lighthouse с помощью GitHub Actions и как решали проблемы, с которыми столкнулись.

VKUI это полноценный UI-фреймворк, с помощью которого можно создавать интерфейсы, внешне неотличимые от тех, которые пользователь видит ВКонтакте. Он адаптивный, а это значит, что приложение на нём будет выглядеть хорошо как на смартфонах с iOS или Android, так и на больших экранах планшетах и даже десктопе. Сегодня VKUI используется практически во всех сервисах платформы VK Mini Apps и важных разделах приложения ВКонтакте, которые надо быстро обновлять независимо от магазинов.

VKUI также активно применяется для экранов универсального приложения VK для iPhone и iPad. Это крупное обновление с поддержкой планшетов на iPadOS мы представили 1 апреля.

Адаптивный экран на VKUIАдаптивный экран на VKUI

Масштабирование было внушительным поэтому мы провели аудит, проанализировали проблемы, которые могут возникнуть у нас и пользователей при работе с VKUI, и заодно сравнили свои решения с продуктами ближайших конкурентов.

Какие задачи поставили

  1. Выявить главные проблемы производительности VKUI.

  2. Подготовить почву, чтобы развернуть автоматизированные бенчмарки библиотеки и наших веб-приложений на основеVKUI.

  3. Сравнить производительность VKUI и конкурирующих UI-фреймворков.

Технологический стек

Инструменты для организации процессов:

Чтобы проще взаимодействовать с несколькими веб-приложениями, в которых применяются разные UI-библиотеки, здесь используем lerna. Это удобный инструмент, с помощью которого мы объединили в большой проект ряд приложений с отличающимися зависимостями.

Бенчмарки мы проводим через Google Lighthouse официальный инструмент для измерения Web Vitals. Де-факто это стандарт индустрии для оценки производительности в вебе.

Самое важное делает GitHub Actions: связывает воедино сборку и аудит наших приложений.

Библиотеки, взятые для сравнения:

Название

Сайт или репозиторий

VKUI

github.com/VKCOM/VKUI

Material-UI

material-ui.com

Yandex UI

github.com/bem/yandex-ui

Fluent UI

github.com/microsoft/fluentui

Lightning

react.lightningdesignsystem.com

Adobe Spectrum

react-spectrum.adobe.com

Ant Design

ant.design

Framework7

framework7.io


Мы решили сравнить популярные UI-фреймворки, часть из которых основана на собственных дизайн-системах. В качестве базового шаблона на React использовали create-react-app, и на момент написания приложений брали самые актуальные версии библиотек.

Тестируемые приложения

Первым делом мы набросали 8 приложений. В каждом были такие страницы:

  1. Default страница с адаптивной вёрсткой, содержит по 23 подстраницы.

    • На главной находится лента с однотипным контентом и предложением ввести код подтверждения (абстрактный). При его вводе на несколько секунд отображается полноэкранный спиннер загрузки.

    • Страница настроек, в которую входит модальное окно с примитивным редактированием профиля и очередным предложением ввести код.

    • Страница с простым диалогом условно, с техподдержкой.

  2. List (Burn) страница со списком из 500 элементов. Главный аспект, который нам хотелось проверить: как вложенность кликабельных элементов влияет на показатель Performance.

  3. Modals страница с несколькими модальными окнами.

Не у всех UI-фреймворков есть аналогичные компоненты недостающие мы заменяли на равнозначные им по функциональности. Ближе всего к VKUI по компонентам и видам их отображения оказались Material-UI и Framework7.

Сделать 8 таких приложений поначалу кажется простой задачей, но спустя неделю просто упарываешься писать одно и то же, но с разными библиотеками. У каждого UI-фреймворка своя документация, API и особенности. С некоторыми я сталкивался впервые. Особенно запомнился Yandex UI кажется, совсем не предназначенный для использования сторонними разработчиками. Какие-то компоненты и описания параметров к ним удавалось найти, только копаясь в исходном коде. Ещё умилительно было обнаружить в компоненте хедера логотип Яндекса <3

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

Автоматизация

Краткая блок-схема, описывающая процессы в автоматизацииКраткая блок-схема, описывающая процессы в автоматизации

Подготовили два воркфлоу:

  • Build and Deploy здесь в первую очередь автоматизировали процессы сборки и разворачивания. Используем surge, чтобы быстро публиковать статичные приложения. Но постепенно перейдём к их запуску и аудиту внутри GitHub Actions воркеров.

  • Run Benchmarks а здесь создаётся issue-тикет в репозитории со ссылкой на активный воркфлоу, затем запускается Lighthouse CI Action по подготовленным ссылкам.

UI-фреймворк

URL на тестовое приложение

VKUI

vkui-benchmark.surge.sh

Ant Design

ant-benchmark.surge.sh

Material UI

mui-benchmark.surge.sh

Framework7

f7-benchmark.surge.sh

Fluent UI

fluent-benchmark.surge.sh

Lightning

lightning-benchmark.surge.sh

Yandex UI

yandex-benchmark.surge.sh

Adobe Spectrum

spectrum-benchmark.surge.sh


Конфигурация сейчас выглядит так:

{  "ci": {    "collect": {      "settings": {        "preset": "desktop", // Desktop-пресет        "maxWaitForFcp": 60000 // Время ожидания ответа от сервера      }    }  }}

После аудита всех приложений запускается задача finalize-report. Она форматирует полученные данные в удобную сравнительную таблицу (с помощью магии Markdown) и отправляет её комментарием к тикету. Удобненько, да?

Пример подобного репорта от 30 марта 2021 г.Пример подобного репорта от 30 марта 2021 г.

Нестабильность результатов

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

Из отчётов Lighthouse выяснили следующее: по неведомым причинам сервер долго отвечал, и это привело к общему замедлению и снижению баллов. Изначально грешили на воркеры и хостинг в целом, и я решился задеплоить и провести бенчмарки на одном сервере. Результаты стали лучше, но такое странное поведение оставалось нелогичным.

Предупреждения из отчётов Google LighthouseПредупреждения из отчётов Google Lighthouse

GitHub Actions по умолчанию выполняет задачи параллельно, как и в нашем воркфлоу с бенчмарками. Решение ограничить максимальное количество выполняющихся одновременно задач:

jobs.<job_id>.strategy.max-parallel: 1

Мы провели бенчмарк дважды, и вуаля результаты стали ощутимо стабильнее. Теперь можно переходить к результатам.

Результаты от 30 марта 2021 г.

VKUI (4.3.0) vs ant:

app

type (app link)

report

performance

vkui (4.3.0)

default

report

0.99

ant

default

report

0.99

vkui (4.3.0)

modals

report

1

ant

modals

report

0.99

vkui (4.3.0)

list

report

0.94

ant

list

report

0.89

list - У ant нет схожего по сложности компонента для отрисовки сложных списков, но на 0,05 балла отстали.

VKUI (4.3.0) vs Framework7:

app

type (app link)

report

performance

vkui (4.3.0)

default

report

0.99

f7

default

report

0.98

vkui (4.3.0)

modals

report

1

f7

modals

report

0.99

vkui (4.3.0)

list

report

0.94

f7

list

report

0.92

list - Framework7 не позволяет вложить одновременно checkbox и radio в компонент списка (List).

VKUI (4.3.0) vs Fluent:

app

type (app link)

report

performance

vkui (4.3.0)

default

report

0.99

fluent

default

report

0.94

vkui (4.3.0)

modals

report

1

fluent

modals

report

0.99

vkui (4.3.0)

list

report

0.94

fluent

list

report

0.97

modals - Разница на уровне погрешности.

list - Fluent не имеет схожего по сложности компонента для отрисовки сложных списков.

VKUI (4.3.0) vs Lightning:

app

type (app link)

report

performance

vkui (4.3.0)

default

report

0.99

lightning

default

report

0.95

vkui (4.3.0)

modals

report

1

lightning

modals

report

1

vkui (4.3.0)

list

report

0.94

lightning

list

report

0.99

list - Lightning не имеет схожего по сложности компонента для отрисовки сложных списков.

VKUI (4.3.0) vs mui:

app

type (app link)

report

performance

vkui (4.3.0)

default

report

0.99

mui

default

report

0.93

vkui (4.3.0)

modals

report

1

mui

modals

report

0.96

vkui (4.3.0)

list

report

0.94

mui

list

report

0.77

default и modals - Расхождение незначительное, у Material-UI проседает First Contentful Paint.

list - При примерно одинаковой загруженности списков в Material-UI и VKUI выигрываем по Average Render Time почти в три раза (~1328,6 мс в Material-UI vs ~476,4 мс в VKUI).

VKUI (4.3.0) vs spectrum:

app

type (app link)

report

performance

vkui (4.3.0)

default

report

0.99

spectrum

default

report

0.99

vkui (4.3.0)

modals

report

1

spectrum

modals

report

1

vkui (4.3.0)

list

report

0.94

spectrum

list

report

1

list - Spectrum не имеет схожего по сложности компонента для отрисовки сложных списков.

VKUI (4.3.0) vs yandex:

app

type (app link)

report

performance

vkui (4.3.0)

default

report

0.99

yandex

default

report

1

vkui (4.3.0)

modals

report

1

yandex

modals

report

1

vkui (4.3.0)

list

report

0.94

yandex

list

report

1

default - Разница на уровне погрешности.

list - Yandex-UI не имеет схожего по сложности компонента для отрисовки сложных списков.

modals - Модальные страницы в Yandex UI объективно легче.

Выводы из отчёта Lighthouse о VKUI

  • VKUI по показателям на одном уровне с другими библиотеками. Сильных просадок, как и значительного превосходства, нет.

  • Одно из явных проблемных мест вложенные Tappable протестированы на большом списке. Единственная библиотека, в которой полноценно реализован этот кейс, Material-UI. И VKUI уверенно обходит её по производительности.

  • Lighthouse ругается на стили после сборки много неиспользуемых. Они же замедляют First Contentful Paint. Над этим уже работают.

Два CSS-чанка, один из которых весит 27,6 кибибайт без сжатия в gzДва CSS-чанка, один из которых весит 27,6 кибибайт без сжатия в gz

Планы на будущее vkui-benchmarks

Переход с хостинга статики на локальное тестирование должен сократить погрешность: уменьшится вероятность того, что из-за внешнего фактора станет ниже балл у того или иного веб-приложения. Ещё у нас в репортах есть показатель CPU/Memory Power и он немного отличается в зависимости от воркеров, которые может дать GitHub. Из-за этого результаты в репортах могут разниться в пределах 0,010,03. Это можно решить введением перцентилей.

Также планируем сделать Lighthouse Server для сохранения статистики по бенчмаркам на каждый релиз.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru