mengapa fungsi terakhir 10% lebih cepat walaupun harus membuat variabel berulang-ulang?

14
var toSizeString = (function() {

 var KB = 1024.0,
     MB = 1024 * KB,
     GB = 1024 * MB;

  return function(size) {
    var gbSize = size / GB,
        gbMod  = size % GB,
        mbSize = gbMod / MB,
        mbMod  = gbMod % MB,
        kbSize = mbMod / KB;

    if (Math.floor(gbSize)) {
      return gbSize.toFixed(1) + 'GB';
    } else if (Math.floor(mbSize)) {
      return mbSize.toFixed(1) + 'MB';
    } else if (Math.floor(kbSize)) {
      return kbSize.toFixed(1) + 'KB';
    } else {
      return size + 'B';
    }
  };
})();

Dan fungsi yang lebih cepat: (perhatikan bahwa ia harus selalu menghitung variabel yang sama kb / mb / gb berulang-ulang). Di mana ia mendapatkan kinerja?

function toSizeString (size) {

 var KB = 1024.0,
     MB = 1024 * KB,
     GB = 1024 * MB;

 var gbSize = size / GB,
     gbMod  = size % GB,
     mbSize = gbMod / MB,
     mbMod  = gbMod % MB,
     kbSize = mbMod / KB;

 if (Math.floor(gbSize)) {
      return gbSize.toFixed(1) + 'GB';
 } else if (Math.floor(mbSize)) {
      return mbSize.toFixed(1) + 'MB';
 } else if (Math.floor(kbSize)) {
      return kbSize.toFixed(1) + 'KB';
 } else {
      return size + 'B';
 }
};
Tomy
sumber
3
Dalam bahasa yang diketik secara statis, "variabel" akan dikompilasi sebagai konstanta. Mungkin mesin JS modern mampu melakukan optimasi yang sama. Ini tampaknya tidak berfungsi jika variabel merupakan bagian dari penutupan.
usr
6
ini adalah detail implementasi mesin JavaScript yang Anda gunakan. Waktu dan ruang teoretisnya sama, hanya penerapan mesin JavaScript yang diberikan yang akan memvariasikan ini. Jadi untuk menjawab pertanyaan Anda dengan benar, Anda perlu membuat daftar mesin JavaScript spesifik yang Anda ukur. Mungkin seseorang tahu detail implementasi itu untuk mengatakan bagaimana / mengapa itu membuat satu lebih optimal daripada yang lain. Anda juga harus memposting kode pengukuran Anda.
Jimmy Hoffa
Anda menggunakan kata "compute" dalam referensi ke nilai konstan; benar-benar tidak ada yang bisa dihitung di sana dalam apa yang Anda referensi. Aritmatika dari nilai konstan adalah salah satu kompiler optimasi yang paling sederhana dan jelas dilakukan, jadi setiap kali Anda melihat ekspresi yang hanya memiliki nilai konstan, Anda dapat mengasumsikan seluruh ekspresi dioptimalkan ke nilai konstan tunggal.
Jimmy Hoffa
@JimmyHoffa itu benar, tetapi di sisi lain itu perlu membuat 3 variabel konstan setiap panggilan fungsi ...
Tomy
Konstanta @Tomy bukan variabel. Mereka tidak bervariasi, sehingga mereka tidak perlu diciptakan kembali setelah kompilasi. Suatu konstanta pada umumnya ditempatkan dalam memori, dan setiap jangkauan di masa depan untuk konstanta itu diarahkan ke tempat yang sama persis, tidak perlu diciptakan ulang karena nilainya tidak akan pernah berubah , oleh karena itu ia bukan variabel. Kompiler pada umumnya tidak akan memancarkan kode yang menciptakan konstanta, kompiler yang membuat dan mengarahkan semua referensi kode untuk apa yang dibuatnya.
Jimmy Hoffa

Jawaban:

23

Mesin JavaScript modern semuanya melakukan kompilasi tepat waktu. Anda tidak dapat membuat anggapan tentang apa yang "harus dibuatnya berulang kali." Perhitungan semacam itu relatif mudah dioptimalkan, dalam kedua kasus tersebut.

Di sisi lain, penutupan variabel konstan bukanlah hal yang biasa Anda targetkan untuk kompilasi JIT. Anda biasanya membuat penutupan ketika Anda ingin dapat mengubah variabel-variabel itu pada permintaan yang berbeda. Anda juga membuat dereference pointer tambahan untuk mengakses variabel-variabel tersebut, seperti perbedaan antara mengakses variabel anggota dan int lokal di OOP.

Situasi seperti inilah yang menyebabkan orang membuang garis "optimasi prematur". Optimalisasi yang mudah sudah dilakukan oleh kompiler.

Karl Bielefeldt
sumber
Saya menduga itu adalah lingkup traversal untuk resolusi variabel yang menyebabkan kerugian seperti yang Anda sebutkan. Tampaknya masuk akal, tetapi siapa yang benar-benar tahu kegilaan apa yang ada dalam mesin JavaScript JIT ...
Jimmy Hoffa
1
Kemungkinan perluasan jawaban ini: alasan JIT akan mengabaikan pengoptimalan yang mudah bagi penyusun offline adalah karena kinerja seluruh penyusun lebih penting daripada kasus yang tidak biasa.
Leushenko
12

Variabelnya murah. Konteks dan rantai lingkup eksekusi mahal.

Ada berbagai jawaban yang pada dasarnya bermuara pada "karena penutupan", dan itu pada dasarnya benar, tetapi masalahnya tidak secara khusus dengan penutupan, itu adalah fakta bahwa Anda memiliki fungsi yang mereferensikan variabel dalam lingkup yang berbeda. Anda akan memiliki masalah yang sama jika ini adalah variabel global di Internetwindow objek, dibandingkan dengan variabel lokal di dalam IIFE. Cobalah dan lihatlah.

Jadi dalam fungsi pertama Anda, ketika mesin melihat pernyataan ini:

var gbSize = size / GB;

Itu harus mengambil langkah-langkah berikut:

  1. Cari variabel sizedalam cakupan saat ini. (Menemukannya.)
  2. Cari variabel GBdalam cakupan saat ini. (Tidak ditemukan.)
  3. Cari variabel GBdalam lingkup induk. (Menemukannya.)
  4. Lakukan perhitungan dan tetapkan gbSize.

Langkah 3 jauh lebih mahal daripada hanya mengalokasikan variabel. Selain itu, Anda melakukan ini lima kali , termasuk dua kali untuk keduanya GBdan MB. Saya menduga bahwa jika Anda menggunakan ini di awal fungsi (misalnya var gb = GB) dan mereferensikan alias, itu sebenarnya akan menghasilkan speedup kecil, meskipun ada juga kemungkinan bahwa beberapa mesin JS sudah melakukan optimasi ini. Dan tentu saja, cara paling efektif untuk mempercepat eksekusi adalah sama sekali tidak melintasi rantai lingkup sama sekali.

Perlu diingat bahwa JavaScript tidak seperti bahasa yang dikompilasi, diketik secara statis di mana kompiler menyelesaikan alamat variabel ini pada waktu kompilasi. Mesin JS harus menyelesaikannya dengan nama , dan pencarian ini terjadi saat runtime, setiap saat. Jadi, Anda ingin menghindarinya jika memungkinkan.

Tugas variabel sangat murah dalam JavaScript. Ini mungkin sebenarnya operasi termurah, walaupun saya tidak mendukung pernyataan itu. Meskipun demikian, aman untuk mengatakan bahwa hampir tidak pernah merupakan ide yang baik untuk mencoba menghindari membuat variabel; hampir semua pengoptimalan yang Anda coba lakukan di bidang itu sebenarnya akan membuat segalanya menjadi lebih buruk, dari segi kinerja.

Aaronaught
sumber
Dan bahkan jika "optimasi" tidak mempengaruhi kinerja negatif, itu hampir pasti adalah akan mempengaruhi pembacaan kode negatif. Yang, kecuali Anda melakukan hal-hal komputasi yang gila, paling sering merupakan tradeoff yang buruk untuk dilakukan (sayangnya tidak ada anchor permalink sayangnya; cari "2009-02-17 11:41"). Seperti rangkumannya: "Pilih kejelasan daripada kecepatan, jika kecepatan tidak mutlak dibutuhkan."
CVn
Bahkan ketika menulis interpreter yang sangat mendasar untuk bahasa dinamis, akses variabel selama runtime cenderung menjadi operasi O (1), dan O (n) lingkup traversal bahkan tidak diperlukan selama kompilasi awal. Di setiap ruang lingkup, setiap variabel yang baru dideklarasikan akan diberi nomor, jadi mengingat var a, b, ckita dapat mengaksesnya bsebagai scope[1]. Semua cakupan diberi nomor, dan jika cakupan ini bersarang sedalam lima cakupan, maka bsepenuhnya ditangani dengan env[5][1]yang diketahui selama penguraian. Dalam kode asli, cakupan terkait dengan segmen stack. Penutupan lebih rumit karena harus mencadangkan dan menggantienv
amon
@amon: Mungkin itu idealnya Anda inginkan , tapi itu bukan cara kerjanya. Orang-orang jauh lebih berpengetahuan dan berpengalaman daripada saya telah menulis buku tentang ini; khususnya saya akan mengarahkan Anda ke JavaScript Kinerja Tinggi oleh Nicholas C. Zakas. Ini cuplikan , dan dia juga melakukan pembicaraan dengan tolok ukur untuk mendukungnya. Tentu saja dia bukan satu-satunya, hanya yang paling terkenal. JavaScript memiliki ruang lingkup leksikal, jadi penutupan sebenarnya tidak terlalu spesial - pada dasarnya, semuanya adalah penutupan.
Aaronaught
@Aonaonaught Menarik. Karena buku itu berumur 5 tahun, saya tertarik bagaimana mesin JS saat ini menangani pencarian variabel dan melihat backend x64 mesin V8. Selama analisis statis, sebagian besar variabel diselesaikan secara statis dan diberikan offset memori dalam cakupannya. Lingkup fungsi direpresentasikan sebagai daftar tertaut, dan perakitan dipancarkan sebagai loop yang tidak gulungan untuk mencapai cakupan yang benar. Di sini, kami akan mendapatkan yang setara dengan kode C *(scope->outer + variable_offset)untuk akses; setiap tingkat ruang lingkup fungsi tambahan biaya satu dereference pointer tambahan. Sepertinya kami berdua benar :)
amon
2

Satu contoh melibatkan penutupan, yang lain tidak. Menerapkan penutupan agak rumit, karena variabel tertutup tidak berfungsi seperti variabel normal. Ini lebih jelas dalam bahasa tingkat rendah seperti C, tapi saya akan menggunakan JavaScript untuk menggambarkan ini.

Penutupan tidak hanya terdiri dari suatu fungsi, tetapi juga dari semua variabel yang ditutup. Ketika kita ingin menjalankan fungsi itu, kita juga perlu menyediakan semua variabel yang ditutup. Kita bisa memodelkan penutupan dengan fungsi yang menerima objek sebagai argumen pertama yang mewakili variabel tertutup ini:

function add(vars, y) {
  vars.x += y;
}

function getSum(vars) {
  return vars.x;
}

function makeAdder(x) {
  return { x: x, add: add, getSum: getSum };
}

var adder = makeAdder(40);
adder.add(adder, 2);
console.log(adder.getSum(adder));  //=> 42

Perhatikan konvensi pemanggilan yang canggung yang closure.apply(closure, ...realArgs)diperlukan

Dukungan objek bawaan JavaScript memungkinkan untuk menghilangkan varsargumen eksplisit , dan thissebagai gantinya kami menggunakan :

function add(y) {
  this.x += y;
}

function getSum() {
  return this.x;
}

function makeAdder(x) {
  return { x: x, add: add, getSum: getSum };
}

var adder = makeAdder(40);
adder.add(2);
console.log(adder.getSum());  //=> 42

Contoh-contoh itu setara dengan kode ini yang benar-benar menggunakan closure:

function makeAdder(x) {
  return {
    add: function (y) { x += y },
    getSum: function () { return x },
  };
}

var adder = makeAdder(40);
adder.add(2);
console.log(adder.getSum());  //=> 42

Dalam contoh terakhir ini, objek hanya digunakan untuk mengelompokkan dua fungsi yang dikembalikan; yang thismengikat tidak relevan. Semua perincian untuk membuat penutupan mungkin - meneruskan data tersembunyi ke fungsi aktual, mengubah semua akses ke variabel penutupan untuk mencari dalam data tersembunyi - diurus oleh bahasa.

Tetapi penutupan panggilan melibatkan overhead melewatkan data tambahan itu, dan menjalankan penutupan melibatkan overhead pencarian dalam data tambahan - diperburuk oleh lokalitas cache yang buruk dan biasanya penunjuk dereferensi bila dibandingkan dengan variabel biasa - sehingga tidak mengherankan bahwa solusi yang tidak bergantung pada penutupan berkinerja lebih baik. Terutama karena semua penutupan Anda menghemat Anda lakukan adalah beberapa operasi aritmatika yang sangat murah, yang bahkan mungkin dilipat konstan selama parsing.

amon
sumber