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

Этот пост создан на основе работы Мартина Клеппе (@aemkei) о преобразовании JS всего в шесть основных символов и сохранении его исполняемости. Это ответ на комментарий Джонатана Минса.

Тем, кто читает это вне контекста, я собираюсь потратить некоторое время на объяснение проблемы, прежде чем переходить к тестам.

Что такое JSF * ck?

За исключением NSFW, это стиль программирования, в котором для написания и выполнения кода на JS используются только шесть символов. Мы можем сделать это благодаря тому, что называется Принуждение типа. JS-принуждение возникает время от времени на JS-конференциях (и в основном для того, чтобы сделать из этого забавное видео), его цель - позволить пользователям работать с разными типами типов без явного преобразования их в один.

const a = "1" + 5 // "15"

Идея прекрасна, но иногда (или, что более вероятно, довольно часто) люди не понимают, как она на самом деле работает под капотом. Для тех, кому интересна идея, вот ссылка на спецификацию http://www.ecma-international.org/ecma-262/#sec-type-conversion.

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

const a = [] + [] // ""

или один из самых известных (+ +"a" возвращает NaN)

// "banana"
const yellowFruit = ("b" + "a" + +"a" + "a").toLocaleLowerCase()

Что с этим делать?

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

// 5+4
[!+[]+!+[]+!+[]+!+[]+!+[]]+(+(+!+[]+(!+[]+[])[!+[]+!+[]+!+[]]+[+!+[]]+[+[]]+[+[]])+[])[!+[]+!+[]]+[!+[]+!+[]+!+[]+!+[]]

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

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

+[] // "0"
+!+[] // "1"
[+!+[]]+[+[]] // "10

Получить струны еще сложнее

[][[]][!+[]+!+[]] // "d" or "undefined"[2]

Он использует тот факт, что к строкам можно обращаться как к массиву символов, и вы можете довольно легко сгенерировать строку «undefined», вызвав [][[]].

Я не буду подробно объяснять каждую из них. LowLevelJS выпустил отличное видео https://www.youtube.com/watch?v=sRWE5tnaxlI, которое освещает эту тему. Если вы хотите поиграть с ним, перейдите на http://www.jsfuck.com/ и попробуйте там свой код.

Влияние на производительность

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

const startT1 = Date.now();

const N = 10000;

let f = { tmp: 3, tmp2: 3, tmp3: 3, tmp4: 3, tmp5: 3, a: 'Gandalf', b: 'The Grey' };
let f2 = { tmp: 3, tmp2: 3, tmp3: 3, tmp4: 3, tmp5: 3, a: 'Jack', b: 'Sparrow' };
let f3 = { tmp: 3, tmp2: 3, tmp3: 3, tmp4: 3, tmp5: 3, a: 'Charles', b: 'Xavier' };
let f4 = { tmp: 3, tmp2: 3, tmp3: 3, tmp4: 3, tmp5: 3, a: 'Frodo', b: 'Baggins' };
let f5 = { tmp: 3, tmp2: 3, tmp3: 3, tmp4: 3, tmp5: 3, a: 'Legolas', b: 'Thranduilion' };
let f6 = { tmp: 3, tmp2: 3, tmp3: 3, tmp4: 3, tmp5: 3, a: 'Indiana', b: 'Jones' };

function test(obj) {
  let result = '';
  for (let i = 0; i < N; i += 1) {
    result += obj.a + obj.b;
  }
  return result;
}

for(let i = 0; i < N; i += 1) {
	test(f);
	test(f2);
	test(f3);
	test(f4);
	test(f5);
	test(f6);
}
console.log("test with one shape:", Date.now() - startT1, "ms.");

Разбирает строку длиной 847192 строк. Таким образом, вместо файла размером ›1 КБ у нас теперь 828 КБ. Вы можете получить этот код Здесь и выполнить его, просто позвонив node index.js.

Пора приступить к тестированию нашего кода !!!

Тестовая среда:

Ubuntu 18.04 
Node 12.13.0 
Intel i7-7820X

Примеры тестов:

Я дважды тестирую стандартную функцию, чтобы увидеть разницу в eval выполнении.

Результаты теста:

стандартный звонок (50 образцов)

AVG: 3274ms 
Std. Dev: 7ms 
Heap: 8.06MB

строка eval (50 образцов)

AVG: 3272ms 
Std. Dev: 6ms 
Heap: 8.08MB

JSF eval (50 примеров):

AVG: 3241ms 
Std. Dev: 8ms 
Heap: 18.88MB

ПРИМЕЧАНИЕ! Значения на вашем компьютере могут отличаться, потому что вы используете другую версию nodeJS или другой процессор.

Выводы

Нет никакой разницы в производительности выполнения между разными версиями одного и того же кода в V8 (узел). Это было ожидаемым, но важным моментом - увидеть объем памяти, используемый V8. Почти нет разницы между оценкой и стандартным вызовом функции, но разбор вашего кода в JSF требует намного больше памяти, чем исходный.

Это не так уж и удивительно, если вы посмотрите на это. Хранение и анализ файла размером почти 1 МБ в JS Engine должны занимать намного больше памяти, чем файл размером 1 КБ.

Даже если код JSF непрактичен, люди могут лучше понять, как работает JS, просто прочитав его часть. Приведение типов - это не волшебство, как многие люди в твиттере. Я знаю, что «Любая достаточно продвинутая технология неотличима от магии» (Первый закон Кларка), но мне нравится перефразировать это: «Любая непонятная технология неотличимы от магии ». Если ты это понимаешь, это уже не волшебство :)

Первоначально опубликовано на https://erdem.pl.