Ketergantungan kinerja kode pada konteks deklarasi variabel dalam JavaScript


Awalnya, artikel ini disusun sebagai tolok ukur kecil untuk penggunaannya sendiri, dan secara umum tidak direncanakan untuk menjadi artikel, namun, dalam proses melakukan pengukuran, beberapa fitur menarik muncul dalam implementasi arsitektur JavaScript yang sangat mempengaruhi kinerja kode akhir dalam beberapa kasus. Saya sarankan, dan Anda, berkenalan dengan hasil yang diperoleh, secara kebetulan juga memeriksa beberapa topik terkait: untuk loop, lingkungan (konteks eksekusi), dan blok.

Pada akhir artikel saya, “Menggunakan deklarasi variabel let dan fitur penutupan JavaScript yang dihasilkan,” saya dengan santai menyentuh topik membandingkan kinerja let (LexicalDeclaration) dan deklarasi variabel var (VarDeclaredNames) dalam loop. Sebagai perbandingan, kami menggunakan runtime manual (tanpa bantuan Array.prototype.sort () ) menyortir array, salah satu metode paling sederhana adalah menyortir berdasarkan pilihan, karena dengan panjang array 100.000 kami mendapat sedikit lebih dari 5 miliar. iterasi dalam dua siklus (eksternal dan bersarang), dan, angka ini seharusnya memungkinkan penilaian yang memadai pada akhirnya.

Untuk var, itu mengurutkan tampilan:

for (var i = 0, len = arr.length; i < len-1; i++) { var min, mini = i; for (var j = i+1; j < len; j++) { if (arr[mini] > arr[j]) mini = j; } min = arr[mini]; arr[mini] = arr[i]; arr[i] = min; } //   Firefox: 9.082 . //   Chrome: 10.783 . 

Dan untuk membiarkan :

 for (let i = 0, len = arr.length; i < len-1; i++) { let min, mini = i; for (let j = i+1; j < len; j++) { if (arr[mini] > arr[j]) mini = j; } min = arr[mini]; arr[mini] = arr[i]; arr[i] = min; } //   Firefox: 5.261 . //   Chrome: 5.391 . 

Melihat angka-angka ini, tampaknya, dapat dengan tegas diperdebatkan bahwa biarkan iklan sepenuhnya melampaui var dalam kecepatan. Tetapi, selain kesimpulan ini, pertanyaan tetap ada di udara: apa yang akan terjadi jika kita membiarkan deklarasi di luar untuk loop?

Tetapi, sebelum melakukan ini, Anda perlu mempelajari lebih dalam pekerjaan for for loop, dipandu oleh spesifikasi ECMAScript 2019 saat ini (ECMA-262) :

 13.7.4.7Runtime Semantics: LabelledEvaluation With parameter labelSet. IterationStatement':'for(Expression;Expression;Expression)Statement 1. If the first Expression is present, then a. Let exprRef be the result of evaluating the first Expression. b. Perform ? GetValue(exprRef). 2. Return ? ForBodyEvaluation(the second Expression, the third Expression, Statement, « », labelSet). IterationStatement':'for(varVariableDeclarationList;Expression;Expression)Statement 1. Let varDcl be the result of evaluating VariableDeclarationList. 2. ReturnIfAbrupt(varDcl). 3. Return ? ForBodyEvaluation(the first Expression, the second Expression, Statement, « », labelSet). IterationStatement':'for(LexicalDeclarationExpression;Expression)Statement 1. Let oldEnv be the running execution context's LexicalEnvironment. 2. Let loopEnv be NewDeclarativeEnvironment(oldEnv). 3. Let loopEnvRec be loopEnv's EnvironmentRecord. 4. Let isConst be the result of performing IsConstantDeclaration of LexicalDeclaration. 5. Let boundNames be the BoundNames of LexicalDeclaration. 6. For each element dn of boundNames, do a. If isConst is true, then i. Perform ! loopEnvRec.CreateImmutableBinding(dn, true). b. Else, i. Perform ! loopEnvRec.CreateMutableBinding(dn, false). 7. Set the running execution context's LexicalEnvironment to loopEnv. 8. Let forDcl be the result of evaluating LexicalDeclaration. 9. If forDcl is an abrupt completion, then a. Set the running execution context's LexicalEnvironment to oldEnv. b. Return Completion(forDcl). 10. If isConst is false, let perIterationLets be boundNames; otherwise let perIterationLets be « ». 11. Let bodyResult be ForBodyEvaluation(the first Expression, the second Expression, Statement, perIterationLets, labelSet). 12. Set the running execution context's LexicalEnvironment to oldEnv. 13. Return Completion(bodyResult). 
catatan: titik dua setelah IterationStatements, di sumbernya tidak dibingkai oleh apostrof - ditambahkan di sini sehingga tidak ada pemformatan otomatis yang cukup banyak merusak keterbacaan teks.

Di sini, seperti yang kita lihat, ada tiga opsi untuk memanggil dan pekerjaan lebih lanjut dari for loop:
  • dengan untuk Pernyataan (Ekspresi; Ekspresi; Ekspresi)
    ForBodyEvaluation (Ekspresi kedua, Ekspresi ketiga, Pernyataan, "", labelSet) .
  • dengan untuk (varVariableDeclarationList; Pernyataan; Ekspresi; Pernyataan)
    ForBodyEvaluation (Ekspresi pertama, Ekspresi kedua, Pernyataan, "", labelSet).
  • pada pernyataan (LexicalDeclarationExpression; Expression)
    ForBodyEvaluation (Ekspresi pertama, Ekspresi kedua, Pernyataan, perIterationLets, labelSet)

Pada varian terakhir, ketiga, tidak seperti dua yang pertama, parameter keempat tidak kosong - perIterationLets - ini sebenarnya deklarasi yang sama pada parameter pertama yang dilewatkan ke for loop. Mereka ditentukan dalam paragraf 10:
- Jika isConst salah , biarkan perIterationLets menjadi boundNames; jika tidak biarkan perIterationLets menjadi "".
Jika sebuah konstanta diteruskan ke untuk , tetapi bukan variabel, parameter perIterationLets menjadi kosong.

Juga, dalam opsi ketiga, perlu memperhatikan paragraf 2:
- Biarkan loopEnv menjadi NewDeclarativeEnvironment (oldEnv).

 8.1.2.2NewDeclarativeEnvironment ( E ) When the abstract operation NewDeclarativeEnvironment is called with a Lexical Environment as argument E the following steps are performed: 1. Let env be a new Lexical Environment. 2. Let envRec be a new declarative Environment Record containing no bindings. 3. Set env's EnvironmentRecord to envRec. 4. Set the outer lexical environment reference of env to E. 5. Return env. 

Di sini, sebagai parameter E , lingkungan dari mana for loop dipanggil (global, fungsi apa saja, dll) diambil, dan, lingkungan baru dibuat untuk mengeksekusi for loop dengan merujuk ke lingkungan eksternal yang menciptakannya (poin 4). Kami tertarik pada fakta ini karena fakta bahwa lingkungan adalah konteks eksekusi.

Dan kita ingat bahwa deklarasi variabel let dan const secara kontekstual terikat pada blok di mana mereka dideklarasikan.

 13.2.14Runtime Semantics: BlockDeclarationInstantiation ( code, env ) Note When a Block or CaseBlock is evaluated a new declarative Environment Record is created and bindings for each block scoped variable, constant, function, or class declared in the block are instantiated in the Environment Record. BlockDeclarationInstantiation is performed as follows using arguments code and env. code is the Parse Node corresponding to the body of the block. env is the Lexical Environment in which bindings are to be created. 1. Let envRec be env's EnvironmentRecord. 2. Assert: envRec is a declarative Environment Record. 3. Let declarations be the LexicallyScopedDeclarations of code. 4. For each element d in declarations, do a. For each element dn of the BoundNames of d, do i. If IsConstantDeclaration of d is true, then 1. Perform ! envRec.CreateImmutableBinding(dn, true). ii. Else, 1. Perform ! envRec.CreateMutableBinding(dn, false). b. If d is a FunctionDeclaration, a GeneratorDeclaration, an AsyncFunctionDeclaration, or an AsyncGeneratorDeclaration, then i. Let fn be the sole element of the BoundNames of d. ii. Let fo be the result of performing InstantiateFunctionObject for d with argument env. iii. Perform envRec.InitializeBinding(fn, fo). 

Catatan: karena dalam dua varian pertama memanggil for loop tidak ada deklarasi seperti itu, tidak perlu membuat lingkungan baru untuk mereka.

Kami melangkah lebih jauh dan mempertimbangkan apa itu ForBodyEvaluation :

 13.7.4.8Runtime Semantics: ForBodyEvaluation ( test, increment, stmt, perIterationBindings, labelSet ) The abstract operation ForBodyEvaluation with arguments test, increment, stmt, perIterationBindings, and labelSet is performed as follows: 1. Let V be undefined. 2. Perform ? CreatePerIterationEnvironment(perIterationBindings). 3. Repeat, a. If test is not [empty], then i. Let testRef be the result of evaluating test. ii. Let testValue be ? GetValue(testRef). iii. If ToBoolean(testValue) is false, return NormalCompletion(V). b. Let result be the result of evaluating stmt. c. If LoopContinues(result, labelSet) is false, return Completion(UpdateEmpty(result, V)). d. If result.[[Value]] is not empty, set V to result.[[Value]]. e. Perform ? CreatePerIterationEnvironment(perIterationBindings). f. If increment is not [empty], then i. Let incRef be the result of evaluating increment. ii. Perform ? GetValue(incRef). 

Apa yang pertama-tama harus Anda perhatikan:
  • deskripsi parameter yang masuk:
    • test : ekspresi diperiksa kebenarannya sebelum iterasi berikutnya dari loop body (misalnya: i <len );
    • increment : ekspresi dievaluasi pada awal setiap iterasi baru (kecuali untuk yang pertama) (misalnya: i ++ );
    • stmt : loop body
    • perIterationBindings : variabel dideklarasikan dengan let in pertama untuk parameter (misalnya: let i = 0 || let i || let i, j );
    • labelSet : label loop;
  • poin 2: di sini, jika parameter non-kosong perIterationBindings dilewati , lingkungan kedua dibuat untuk melakukan pass awal dari loop;
  • paragraf 3.a: memeriksa kondisi yang diberikan untuk melanjutkan pelaksanaan siklus;
  • klausa 3.b: eksekusi dari badan siklus;
  • poin 3.e: menciptakan lingkungan baru.

Nah, dan, secara langsung, algoritma untuk menciptakan lingkungan internal for for :

 13.7.4.9Runtime Semantics: CreatePerIterationEnvironment ( perIterationBindings ) 1. The abstract operation CreatePerIterationEnvironment with argument perIterationBindings is performed as follows: 1. If perIterationBindings has any elements, then a. Let lastIterationEnv be the running execution context's LexicalEnvironment. b. Let lastIterationEnvRec be lastIterationEnv's EnvironmentRecord. c. Let outer be lastIterationEnv's outer environment reference. d. Assert: outer is not null. e. Let thisIterationEnv be NewDeclarativeEnvironment(outer). f. Let thisIterationEnvRec be thisIterationEnv's EnvironmentRecord. g. For each element bn of perIterationBindings, do i. Perform ! thisIterationEnvRec.CreateMutableBinding(bn, false). ii. Let lastValue be ? lastIterationEnvRec.GetBindingValue(bn, true). iii. Perform thisIterationEnvRec.InitializeBinding(bn, lastValue). h. Set the running execution context's LexicalEnvironment to thisIterationEnv. 2. Return undefined. 

Seperti yang bisa kita lihat, paragraf pertama memeriksa keberadaan elemen apa pun dalam parameter yang diteruskan, dan paragraf 1 hanya dilakukan jika ada pengumuman. Semua lingkungan baru dibuat dengan referensi ke konteks eksternal yang sama dan mengambil nilai terbaru dari iterasi sebelumnya (lingkungan kerja sebelumnya) sebagai binding baru variabel let .

Sebagai contoh, perhatikan ungkapan serupa:

 let arr = []; for (let i = 0; i < 3; i++) { arr.push(i); } console.log(arr); // Array(3) [ 0, 1, 2 ] 

Dan di sini adalah bagaimana ia dapat didekomposisi tanpa menggunakan untuk (dengan sejumlah konvensionalitas):

 let arr = []; //    { let i = 0; //     for } //   ,   { let i = 0; //    i    if (i < 3) arr.push(i); } //    { let i = 0; //    i    i++; if (i < 3) arr.push(i); } //    { let i = 1; //    i    i++; if (i < 3) arr.push(i); } //    { let i = 2; //    i    i++; if (i < 3) arr.push(i); } console.log(arr); // Array(3) [ 0, 1, 2 ] 

Bahkan, kami sampai pada kesimpulan bahwa untuk setiap konteks, dan di sini kami memiliki lima dari mereka, kami membuat binding baru untuk membiarkan variabel dideklarasikan sebagai parameter pertama dalam untuk (penting: ini tidak berlaku untuk membiarkan deklarasi langsung di tubuh loop).

Begini caranya, misalnya, loop ini akan terlihat ketika menggunakan var ketika tidak ada binding tambahan:

 let arr2 = []; var i = 0; if (i < 3) arr.push(i); i++; if (i < 3) arr.push(i); i++; if (i < 3) arr.push(i); i++; if (i < 3) arr.push(i); console.log(arr); // Array(3) [ 0, 1, 2 ] 

Dan kita bisa sampai pada kesimpulan yang tampaknya logis bahwa jika selama pelaksanaan loop kita tidak perlu membuat binding terpisah untuk setiap iterasi ( lebih lanjut tentang situasi di mana ini, sebaliknya, mungkin masuk akal ), kita harus membuat deklarasi variabel tambahan sebelum dengan for for , yang seharusnya menyelamatkan kita dari menciptakan dan menghapus sejumlah besar konteks dan, secara teori, meningkatkan kinerja.

Mari kita coba melakukan ini, dengan menggunakan pengurutan yang sama dari 100.000 elemen sebagai contoh, dan demi kecantikan, kami juga membuat definisi dari semua variabel lain sebelumnya untuk :

 let i, j, min, mini, len = arr.length; for (i = 0; i < len-1; i++) { mini = i; for (j = i+1; j < len; j++) { if (arr[mini] > arr[j]) mini = j; } min = arr[mini]; arr[mini] = arr[i]; arr[i] = min; } //   Firefox: 34.246 . //   Chrome: 10.803 . 

Hasil yang tidak terduga ... Justru kebalikan dari apa yang diharapkan, tepatnya. Drawdown Firefox dalam tes ini sangat mencolok.

Ok Ini tidak berhasil, mari kita kembalikan deklarasi variabel i dan j kembali ke parameter dari siklus yang sesuai:

 let min, mini, len = arr.length; for (let i = 0; i < len-1; i++) { mini = i; for (let j = i+1; j < len; j++) { if (arr[mini] > arr[j]) mini = j; } min = arr[mini]; arr[mini] = arr[i]; arr[i] = min; } //   Firefox: 6.575 . //   Chrome: 6.749 . 

Hm Tampaknya, secara teknis, satu-satunya perbedaan antara contoh terakhir dan contoh di awal artikel adalah deklarasi variabel yang dibuat min, mini dan len di luar for loop, dan meskipun perbedaannya masih kontekstual, itu tidak terlalu menarik bagi kita sekarang, dan, di samping itu, kami menghilangkan kebutuhan untuk mendeklarasikan variabel-variabel ini 99.999 kali dalam tubuh siklus tingkat atas, yang lagi-lagi, secara teori, harus meningkatkan produktivitas daripada menguranginya lebih dari satu detik.

Yaitu, ternyata entah bagaimana, bekerja dengan variabel yang dideklarasikan dalam parameter atau badan for loop terjadi jauh lebih cepat daripada di luarnya.

Tapi, kami sepertinya tidak melihat instruksi "turbo" dalam spesifikasi for for loop yang dapat mengarahkan kami ke ide semacam itu. Oleh karena itu, ini bukan spesifik pekerjaan loop for khusus, tetapi sesuatu yang lain ... Misalnya, fitur deklarasi let : apa fitur utama yang membedakan let dari var ? Cekal konteks pelaksanaan! Dan dalam dua contoh terakhir kami, kami menggunakan iklan di luar blok. Tetapi, bagaimana jika alih-alih memindahkan deklarasi ini kembali ke , kami hanya memilih blok terpisah untuknya?

 { let i, j, min, mini, len = arr.length; for (i = 0; i < len-1; i++) { mini = i; for (j = i+1; j < len; j++) { if (arr[mini] > arr[j]) mini = j; } min = arr[mini]; arr[mini] = arr[i]; arr[i] = min; } } //   Firefox: 5.262 . //   Chrome: 5.405 . 

Voila! Ternyata tangkapannya adalah membiarkan pengumuman dilakukan dalam konteks global, dan segera setelah kami mengalokasikan blok terpisah untuk mereka, semua masalah menghilang di sana.

Dan di sini akan lebih baik untuk mengingat cara lain, sedikit terkutuk dikutuk untuk mendeklarasikan variabel - var .

Dalam contoh di awal artikel, waktu menyortir menggunakan var menunjukkan hasil yang sangat menyedihkan, relatif dibandingkan dengan membiarkan . Tetapi, jika Anda melihat lebih dekat pada contoh ini, Anda mungkin menemukan bahwa, karena var tidak memiliki ikatan blok variabel, konteks sebenarnya dari variabel adalah global. Dan kami, pada contoh let , telah menemukan bagaimana ini dapat mempengaruhi kinerja (dan, yang khas, ketika menggunakan let , drawdown dalam kecepatan ternyata lebih kuat daripada dalam kasus dengan var , terutama di Firefox ). Oleh karena itu, dalam keadilan, kami akan menjalankan contoh dengan var membuat konteks baru untuk variabel:

 function test() { var i, j, min, mini, len = arr.length; for (i = 0; i < len-1; i++) { mini = i; for (j = i+1; j < len; j++) { if (arr[mini] > arr[j]) mini = j; } min = arr[mini]; arr[mini] = arr[i]; arr[i] = min; } } test(); //   Firefox: 5.255 . //   Chrome: 5.411 . 

Dan, kami mendapat hasil yang hampir identik dengan apa yang digunakan saat menggunakan let .

Akhirnya, mari kita periksa apakah perlambatan terjadi dengan membaca variabel global tanpa mengubah nilainya.

biarkan

 let len = arr.length; for (let i = 0; i < len-1; i++) { let min, mini = i; for (let j = i+1; j < len; j++) { if (arr[mini] > arr[j]) mini = j; } min = arr[mini]; arr[mini] = arr[i]; arr[i] = min; } //   Firefox: 5.262 . //   Chrome: 5.391 . 

var

 var len = arr.length; function test() { var i, j, min, mini; for (i = 0; i < len-1; i++) { mini = i; for (j = i+1; j < len; j++) { if (arr[mini] > arr[j]) mini = j; } min = arr[mini]; arr[mini] = arr[i]; arr[i] = min; } } test(); //   Firefox: 5.258 . //   Chrome: 5.439 . 

Hasil menunjukkan bahwa membaca variabel global tidak mempengaruhi waktu eksekusi.

Untuk meringkas


  1. Mengubah variabel global jauh lebih lambat daripada mengubah variabel lokal. Dengan mempertimbangkan hal ini, dimungkinkan untuk mengoptimalkan kode dalam situasi yang sesuai dengan membuat blok atau fungsi terpisah, termasuk untuk mendeklarasikan variabel, alih-alih mengeksekusi bagian dari kode dalam konteks global. Ya, di hampir semua buku teks, Anda dapat menemukan rekomendasi untuk membuat ikatan global sesedikit mungkin, tetapi biasanya hanya menyumbat ruang nama global yang diindikasikan sebagai alasan, dan bukan sepatah kata pun tentang kemungkinan masalah kinerja.
  2. Terlepas dari kenyataan bahwa pelaksanaan loop dengan deklarasi let di parameter pertama untuk menciptakan sejumlah besar lingkungan, ini hampir tidak berpengaruh pada kinerja, tidak seperti situasi ketika kita mengambil deklarasi seperti itu di luar blok. Namun demikian, orang tidak boleh mengecualikan kemungkinan keberadaan situasi eksotis ketika faktor ini akan mempengaruhi produktivitas secara lebih signifikan.
  3. Kinerja variabel var masih tidak kalah dengan variabel biarkan , namun, itu tidak melebihi mereka (sekali lagi, dalam kasus umum), yang membawa kita pada kesimpulan berikutnya bahwa tidak ada alasan untuk menggunakan deklarasi var kecuali untuk tujuan kompatibilitas. Namun, jika perlu untuk memanipulasi variabel global dengan mengubah nilai-nilai mereka, varian dengan var dalam hal kinerja akan lebih disukai (setidaknya untuk saat ini, jika, khususnya, diasumsikan bahwa skrip juga dapat dijalankan pada mesin Gecko).

Referensi


ECMAScript 2019 (ECMA-262)
Menggunakan deklarasi biarkan variabel dan fitur dari penutupan yang dihasilkan dalam JavaScript

Source: https://habr.com/ru/post/id469523/


All Articles