Проверка дали начинът, по който пишем JS, може да промени производителността на неговото изпълнение, дори ако кодът е същият?

Тази публикация е създадена въз основа на работата на Мартин Клепе (@aemkei) за преобразуването на JS само в шест основни знака и запазването му в изпълним вид. Това е отговор на коментар от Jonathan Means.

За тези, които четат това извън контекста, ще прекарам известно време в обяснение на проблема, преди да премина към тестове.

Какво е JSF*ck?

Освен че е NSFW, това е стил на програмиране, който използва само шест знака за писане и изпълнение на код в JS. Можем да направим това поради sth, наречено Принуда на типа. 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 (включително оценка). Ако можем да направим това, тогава трябва да можем да конвертираме нашия код в sth като:

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

Постигането на това не е толкова лесно, колкото си мислите.

За да получите число като низ, можете да използвате

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

Получаването на низове е още по-трудно

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

Той използва факта, че низовете могат да бъдат достъпни като масив от знаци и можете да генерирате „недефиниран“ низ доста лесно, като извикате [][[]].

Няма да обяснявам всеки един от тях подробно. Има страхотно видео, пуснато от 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 реда. Така че вместо ›1KB файл, сега имаме 828KB. Можете да получите този код тук и да го изпълните само като извикате node index.js.

Време е да започнем да тестваме нашия код!!!

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

Ubuntu 18.04 
Node 12.13.0 
Intel i7-7820X

Примери за тестове:

Тествам стандартна функция два пъти, за да мога да забележа разликата в изпълнението на eval.

Резултати от теста:

стандартно обаждане (50 проби)

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

оценка на низ (50 проби)

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

JSF оценка (50 проби):

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

ЗАБЕЛЕЖКА! Стойностите на вашата машина може да са различни, защото използвате различна версия на nodeJS или различен процесор.

Изводи

Няма разлика в производителността на изпълнение между различните версии на един и същ код във V8 (възел). Това беше очаквано, но важно нещо да се види тук е количеството памет, използвано от V8. Няма почти никаква разлика между оценка и стандартно извикване на функция, но анализирането на вашия код в JSF отнема много повече памет от оригиналния.

Не е толкова изненадващо, ако го погледнете. Съхраняването и анализирането на почти 1MB файл в JS Engine трябва да отнеме много повече памет от 1KB файл.

Дори ако JSF кодът не е практичен, хората могат да разберат по-добре как работи JS, просто като прочетат част от него. Типовата принуда не е някаква магия, както казват много хора в Twitter. Знам, че „Всяка достатъчно напреднала технология е неразличима от магията“ (Първият закон на Кларк), но обичам да перифразирам това: „Всяка неразбираема технология е неразличими от магия”. Ако можете да го разберете, това вече не е магия :)

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