erjigit17 / habra_palindromes

для статьи

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Ускоряем JS до предела C

Описание

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

Базовая Функция

Начну с простой и выразительной функции на JavaScript:

const isPalindromeJSbase = (s) => {
    const clear = s.replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
    return clear === [...clear].reverse().join('');
}

Эта функция удаляет все неалфавитно-цифровые символы, приводит строку к нижнему регистру и проверяет, является ли строка палиндромом, сравнивая ее с перевернутой версией.

Оптимизация 1: Ускорение за счет исключения ненужных преобразований

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

'use strict';

export function isPalindromeJSFast(s) {
  let left = 0;
  let right = s.length - 1;

  while (left < right) {
      while (left < right && !isAlphanumeric(s[left])) {
          left++;
      }
      while (left < right && !isAlphanumeric(s[right])) {
          right--;
      }
      if (s[left] !== s[right]) {
          if (s[left].toLowerCase() !== s[right].toLowerCase()) {
              return false;
          }
      }
      left++;
      right--;
  }
  return true;
}

function isAlphanumeric(c) {
  return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
}

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

Оптимизация 2: Допущение об одинаковом регистре

Делаю допущение, что в большинстве случаев строки будут в одном регистре, и только при несовпадении привожу их к единому регистру:

      if (s[left] !== s[right]) {
          if (s[left].toLowerCase() !== s[right].toLowerCase()) {
              return false;
          }
      }
      left++;
      right--;

Эта оптимизация позволяет ускорить выполнение функции в 7 раз по сравнению с базовой.

Оптимизация 3: Использование C для максимальной производительности

С помощью ChatGPT я написал модуль на C, который показывает уже 20-кратное улучшение производительности по сравнению с базовой функцией. Данный модуль можно использовать в JS-коде. Также можно использовать в TypeScript, покрыв типами.

Оптимизация 4: Использование SIMD-инструкций

На MacBook Air M1 с использованием SIMD-инструкций (аналоги AVX в x86)удалось добиться ускорения в 224 раза по сравнению с базовой функцией. Работоспособность в x86 не проверял, в коде есть условия для перевода в x86.

Результаты

  • isPalindromeJSbase: базовая производительность.
  • isPalindromeJSFast: ускорение в 4.5 раза.
  • isPalindromeJSSuperFast: ускорение в 7 раз.
  • C-модуль: ускорение в 20 раз.
  • SIMD-инструкции: ускорение в 224 раза (на MacBook Air M1).

Заключение

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

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

Инструкция

Установка и сборка модуля для работы с языком C

Установите зависимости для работы с нативными модулями: npm install node-addon-api Сборка модуля: npx node-gyp clean npx node-gyp configure npx node-gyp build

Настройка окружения

.env.example -> .env

Скрипты для запуска

Создание строк для тестирования производительности: npm run seed

Запуск тестирования: npm start

Условия тестирования

Данные для тестирования брались с учетом того, что длина строки была 10_000_000 символов, а время замеров производилось за 50 прогонов. Перед замером времени выполнения коду давалась возможность "подогреться", чтобы оптимизаторы смогли сделать необходимые оптимизации. Перед тестом MacBook Air M1 давали остыть.

224-кратное ускорение — это очень большая разница, но она важна при очень больших значениях. В реальных условиях такой производительности достичь сложно, особенно на небольших данных, как жонглирование с JSON-ками, где оптимизация особо не поможет. Например, бекенд на Nest.js создает столько лишних телодвижений, что мои вкрапления кода кажутся мизерными. Пока до меня доходит HTTP-запрос, созданный библиотекой node:http, на него накладывают логику от Express.js, далее Nest.js оборачивает их в Observable (RxJS).

Например, в бенчмарке TechEmpower:

  • Node.js получает 64 779 очков
  • Fastify — 56 794 очков
  • Nest.js — 35 814 очков (вероятно под капотом Express.js)

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

Разработчик среднего уровня на JavaScript может написать код, который будет лишь в 2-3 раза медленнее C (что очень даже хорошо). Если же важна исключительная производительность, то добро пожаловать в мир C, где уже придется учитывать архитектуры процессоров, но зато с помощью магии оптимизации на низкоуровневом C можно достичь значительного ускорения, на которое компиляторы JavaScript не способны. Это не значит, что я откажусь от регулярных выражений. С лавинообразным ростом использования эмоджи, только благодаря новым функциям в regex можно точно определить длину строк, содержащих эмоджи, и выполнять текстовую верстку (если я прав). Операторы rest и spread - это дар с небес, позволяющий не запоминать новые методы массивов, объектов или строк.

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

About

для статьи


Languages

Language:JavaScript 55.7%Language:C 38.9%Language:Python 5.4%