Tentang metode yang dapat diubah dari objek Matematika dalam JavaScript

Hari ini kami menerbitkan terjemahan artikel tentang perhitungan matematis dalam JavaScript, yang merupakan versi tertulis dari presentasi penulisnya di WaffleJS . Dan pernyataan ini sendiri merupakan kelanjutan dari percakapan ini di Twitter.


Pendidikan matematika

Minat dalam matematika


Semuanya dimulai dengan pendidikan matematika saya, dengan penerimaan yang agak tertunda. Ketika saya belajar ilmu komputer, saya bisa berkomunikasi dengan guru matematika kelas dunia, tetapi saya melewatkan kesempatan ini. Saya tidak suka matematika: topik yang dipelajari sangat jauh dari praktik. Selain itu, saya sudah tidak seimbang dengan program pelatihan komputer yang sangat teoretis. Saya kemudian percaya bahwa dia telah bercerai dari kehidupan, dan, sebagian besar, saya terus berpikir demikian.

Tetapi sekarang, beberapa tahun setelah saya menyelesaikan studi saya, kehausan untuk belajar matematika terbangun dalam diri saya. Saya terinspirasi oleh apa efek bahkan aplikasi kecil dari pengetahuan matematika dapat pada pekerjaan saya dan hobi saya. Tetapi saya tidak memiliki rencana studi yang jelas.

Kemudian, pada 2012, saya menemukan cara untuk belajar matematika dengan mulai bekerja pada proyek Statistik Sederhana . Sejak itu saya telah memperluas dan mendukung proyek ini. Sekarang ini termasuk implementasi dari banyak algoritma, itu ternyata menjadi salah satu perpustakaan JavaScript matematis yang paling " berbintang ". Dan, ternyata, orang benar-benar menggunakan perpustakaan ini.

Tapi saya mulai bekerja pada 2012. Jika kita berbicara tentang bagaimana teknologi telah berubah dari waktu ke waktu, maka itu adalah waktu yang sangat, sangat lama. Sejak itu, 8 rilis Node.js LTS telah dirilis . Sejak itu, JavaSript sendiri dan lingkungan di mana program yang ditulis dalam karya bahasa ini telah banyak berubah. Pada 2012, perpustakaan Bereaksi belum ada, maka komitmen pertama untuk proyek Babel belum dibuat.


Berlalunya waktu

Selama bertahun-tahun, saya perhatikan bahwa pengujian saya macet ketika memperbarui Node.js. Misalnya, saya mungkin memiliki tes seperti ini:

t.equal(ss.gamma(11.54), 13098426.039156161);

Tes ini berfungsi dengan baik di Node.js v10, tetapi pecah di Node.js v12. Dan ini bukan metode super rumit yang diuji: fungsinya gammadiimplementasikan menggunakan fungsi JavaScript standar - Math.pow, Math.sqrtdan Math.sin.

Hitung


Saya tahu apa yang dapat Anda pikirkan di sini: aritmatika. Di Twitter, diskusi yang memanas secara berkala memuncak karena hasil penghitungan ekspresi berikut:

0.1 + 0.2 = 0.30000000000000004

Tapi, seperti yang sudah saya tulis , semua bahasa pemrograman populer berperilaku seperti ini, bahkan yang kuno dan bertele-tele seperti Haskell. Aritmatika titik-mengambang mungkin terlihat aneh, tetapi mereka berperilaku sama, perilaku mereka didokumentasikan dengan baik. Yaitu, kita berbicara tentang standar IEEE 754 , persyaratan yang diterapkan secara ketat dalam bahasa pemrograman. Jadi, maka masalahnya bukan aritmatika: implementasi penjumlahan, pengurangan, pembagian dan perkalian dalam bahasa, pemrograman dapat dikatakan “diukir dalam batu”.

Objek matematika


Masalah saya ada di objek JavaScript standar Math. Secara khusus, dalam semua metode objek ini.

Teknik seperti Math.sin, Math.cos, Math.exp, Math.pow, Math.tan- adalah bahan dasar untuk geometri dan lainnya perhitungan. Ketika saya memahami hal ini, saya mulai mempelajari secara terpisah perubahan perilaku metode dasar objek Mathdalam versi Node.js. yang berbeda Berikut ini beberapa contohnya.

Perhitungan Math.tanh(0.1):

// Node 4
0.09966799462495590234
// Node 6
0.09966799462495581907

Perhitungan Math.pow(1/3, 3):

// Node 10
0.03703703703703703498
// Node 12
0.03703703703703702804

Lebih buruk lagi, masalah ini tidak hanya terlihat di Node.js. Hal yang sama terjadi di browser, dan di lingkungan lain yang mendukung JavaScript.

Ini membawa kita pada pertanyaan berikut: apa itu perhitungan matematika?


Representasi grafis dari perhitungan

Metode trigonometri mudah divisualisasikan. Jika Anda memiliki satu lingkaran dan beberapa bulan sekolah menengah yang Anda inginkan, maka Anda tahu bahwa cosinus dan sinus adalah koordinat titik tertentu di tepi lingkaran, dan grafik fungsi dosa dan cos terlihat seperti gelombang. Bahkan, di sekolah menengah mereka mempelajari derivasi dari nilai-nilai ini, tetapi metode yang digunakan untuk ini - seri Taylor - bergantung pada seri yang tak terbatas, dan tidak mudah bagi komputer untuk memecahkan masalah seperti itu.

Inilah yang dapat Anda pelajari dari Wikipediamengenai algoritma perhitungan sinus: “Tidak ada algoritma standar untuk menghitung sinus. IEEE 754-2008, standar yang paling banyak digunakan untuk perhitungan floating point, tidak mempengaruhi perhitungan fungsi trigonometri seperti sinus. "

Komputer menggunakan banyak pendekatan dan algoritma yang berbeda untuk melakukan perhitungan, seperti CORDIC , segala macam trik rumit dan tabel pencarian. Semua heterogenitas ini menjelaskan keberadaan banyak perpustakaan fastmath di GitHub . Faktanya adalah ada banyak cara untuk mengimplementasikan metode ini Math.sin. Dan fungsi lainnya juga. Misalnya, seperti yang Anda tahu, Quake III Arena menggunakan yang lebih cepatmengganti metode perhitungan akar kuadrat terbalik standar untuk mempercepat rendering.

Akibatnya, perhitungan matematis adalah hasil dari penerapan algoritma tertentu. Dalam praktiknya, banyak algoritma umum dan variannya digunakan.

Spesifikasi JavaScript, alih-alih menentukan algoritma tertentu yang akan digunakan dalam implementasi bahasa, memberikan banyak ruang bagi implementasi untuk manuver dalam hal fungsi yang digunakan dalam perhitungan matematis.

Inilah yang dikatakan standar tentang ini (ECMA-262, edisi 10, bagian 20.2.2):

"Perilaku fungsi acos, acosh, asin, asinh, atanh, atan2, cbrt, cos, cosh, exp, expm1, hypot, log, log1p, log2, log10, pow, acak, dosa, sinh, sqrt, tan dan tanh itu tidak sepenuhnya dijelaskan di sini, dengan pengecualian persyaratan untuk mengembalikan hasil tertentu untuk nilai-nilai spesifik dari argumen, yang merupakan kasus batas yang patut diperhatikan. "

Saya tidak tahu bagaimana aktivitas internal anggota komite yang bertanggung jawab untuk pekerjaan standar ECMA-262, tetapi saya percaya bahwa mereka membuat standar sehingga tidak akan ada krisis kompatibilitas dalam JavaScript jika Intel atau AMD mengeluarkan instruksi matematis ultrafast baru dalam prosesor segar mereka.

Karena kenyataan bahwa ada banyak penerjemah JavaScript yang banyak digunakan, karena fakta bahwa JavaScript sering digunakan di peramban, dan di antara peramban masih ada sesuatu seperti kompetisi, dan karena kenyataan bahwa bahkan implementasi JavaScript yang populer berada di bawah tekanan konstan dan dipaksa untuk berevolusi dengan cepat, memberikan kinerja terbaik ... karena semua ini, kita memiliki apa yang kita miliki. Siapa pun yang menggunakan JavaScript akan secara teratur mengalami kenyataan bahwa dalam implementasi yang berbeda, hasil perhitungan matematis yang dilakukan dengan cara objek Mathberbeda.

Ini tidak begitu penting dalam bahasa lain yang ditafsirkan, karena mereka biasanya memiliki beberapa implementasi "kanonik". Misalnya, ini berlaku untuk interpreter Python.

Di mana perhitungan dilakukan?


Sekarang mari kita lihat lebih dekat di mana tepatnya perhitungan dilakukan. Dalam kasus JavaScript, ada tiga area tempat perhitungan matematika dasar dilakukan:

  1. CPU.
  2. Penerjemah bahasa (kode C ++ dan C dari implementasi JavaScript tertentu).
  3. Kode JavaScript, misalnya, kode untuk perpustakaan khusus.

▍1. CPU


Ide pertama yang muncul di benak saya ketika saya memikirkan tempat-tempat di mana perhitungan dilakukan adalah bahwa perhitungan dilakukan di prosesor. Saya menyarankan bahwa karena prosesor menerapkan perhitungan aritmatika, mereka juga dapat menerapkan beberapa perhitungan yang lebih kompleks. Ternyata prosesor tersebut memiliki instruksi untuk melakukan perhitungan trigonometri dan lainnya, tetapi instruksi ini jarang digunakan. Sebagai contoh, implementasi perhitungan sinus pada prosesor dengan arsitektur x86 tidak terlalu populer, karena implementasi ini tidak selalu berubah menjadi lebih cepat daripada implementasi perangkat lunak (yang menggunakan operasi aritmatika prosesor). Selain itu, belum tentu lebih akurat daripada implementasi perangkat lunak.

Intel juga menderita rasa malu karena sangat kuatmenaksir terlalu tinggi keakuratan operasi trigonometri dalam dokumentasi. Kesalahan seperti itu sangat tragis karena fakta bahwa microchip, tidak seperti program, tidak dapat ditambal.

▍2. Penerjemah bahasa


Ini adalah cara kerja subsistem komputasi di sebagian besar implementasi JavaScript. Mereka menerapkan subsistem ini dengan berbagai cara.

  • Mesin V8 dan SpiderMonkey menggunakan port library fdlibm untuk perhitungan , yang sedikit berbeda. Perpustakaan ini, awalnya ditulis di Sun Microsystems, telah diturunkan dari generasi ke generasi.
  • JavaScriptCore (Safari) menggunakan perpustakaan cmath untuk melakukan sebagian besar operasi.
  • Internet Explorer menggunakan cmath dan beberapa blok kode yang ditulis dalam assembler . Di sini bahkan metode trigonometri prosesor digunakan - dalam hal browser dikompilasi untuk prosesor yang memiliki instruksi serupa.

Untuk alasan historis, alat yang digunakan untuk melakukan perhitungan di mesin JS yang berbeda telah berubah. Jadi, V8 menggunakan solusi sendiri untuk perhitungan, kemudian menggunakan port fdlibm JavaScript , dan hanya kemudian versi C dari fdlibm.

▍ Mengapa ini menjadi masalah?


Intinya di sini adalah bahwa semua ini mengurangi kemampuan JavaScript untuk menghasilkan hasil yang seragam dalam menyelesaikan masalah yang melibatkan perhitungan matematika. Ini sangat sulit pada Ilmu Data. Saya ingin JavaScript lebih cocok untuk melakukan perhitungan Ilmu Data di browser. Selain itu, ketidakmampuan untuk menghasilkan hasil yang seragam berarti memperburuk krisis reproduktifitas , yang merupakan karakteristik dari semua ilmu. Ini belum lagi beberapa masalah JavaScript lainnya, seperti fitur nomor pengetikan dan kurangnya perpustakaan yang banyak digunakan untuk bekerja dengan bingkai data.

▍3. Menggunakan perpustakaan khusus


Ada cara yang dapat diandalkan bagi kita untuk melakukan perhitungan dalam JavaScript. Itu terdiri dalam menggunakan perpustakaan khusus. Jadi, perpustakaan stdlib mengimplementasikan perhitungan tingkat tinggi hanya menggunakan operasi aritmatika. Perhitungan aritmatika sepenuhnya dijelaskan dalam spesifikasi, mereka adalah standar, sehingga hasil yang dikembalikan oleh stdlib memberi kita, terlepas dari platform tempat kode dijalankan, hasil yang sepenuhnya seragam.

Ini dicapai dengan biaya kompleksitas dan kecepatan pengambilan keputusan. Metode stdlib tidak secepat metode bawaan. Selain itu, untuk "hitung saja sinus", Anda perlu menghubungkan seluruh perpustakaan ke proyek.

Tetapi, jika Anda berpikir lebih luas, ini sepenuhnya normal. Platform WebAssembly, misalnya, tidak memberikan programmer cara untuk melakukan perhitungan matematika tingkat tinggi. Dalam dokumentasi untuk itu, Anda disarankan untuk secara mandiri menyertakan implementasi dari mekanisme yang sesuai dalam modul Anda sendiri:

“WebAssembly tidak menyertakan implementasi sendiri dari fungsi matematika - seperti sin, cos, exp, pow, dan sebagainya. Strategi WebAssembly untuk fungsi-fungsi tersebut adalah untuk memungkinkan pengembang mengimplementasikannya sebagai alat pustaka di platform WebAssembly itu sendiri (perhatikan bahwa instruksi x dan cos platform x86 lambat dan tidak akurat, dan hari ini, bagaimanapun, cobalah untuk tidak menggunakan). "

Begitulah cara bahasa yang dikompilasi selalu bekerja: ketika menyusun program yang ditulis dalam C, metode yang diimpor dari math.htermasuk dalam program yang dikompilasi.

Menggunakan Nilai epsilon


Jika seseorang tidak ingin memasukkan perpustakaan stdlib dalam proyek JavaScript-nya untuk melakukan perhitungan, tetapi perlu menguji kode yang melakukan beberapa perhitungan kompleks, maka ia mungkin harus menggunakan metode yang sudah digunakan di perpustakaan statistik sederhana. Ini adalah tentang menggunakan nilai epsilonyang mendefinisikan batas-batas di mana perbedaan angka tidak diperhitungkan. Jika kita mempertimbangkan opsi untuk menggunakan simbol epsilon dalam matematika, maka kita dapat mengatakan bahwa saya membicarakannya sebagai “nilai positif kecil yang sewenang-wenang”. Statistik sederhana menggunakan nilai epsilon sama 0.0001.

Jika Anda perlu mengetahui apakah dua angka sama, kondisi formulir diperiksaMath.abs(result — expected) < epsilon. Jika kondisi ini ternyata benar, maka kita dapat mengatakan bahwa perbedaan antara angka berada dalam kisaran yang diberikan dan menganggapnya sama.

Penambahan


▍ Akurasi


Komentator di Twitter menunjukkan bahwa variasi dalam hasil yang diperoleh dalam contoh di luar kisaran angka signifikan dari angka floating point. Dari sudut pandang teknis, ini benar, dan ini berarti bahwa Anda dapat menemukan cara yang lebih akurat untuk membandingkan angka daripada yang menggunakan nilai epsilon. Namun dalam praktiknya cerita yang sama di sini - angka-angka di akhir angka mempengaruhi hasil dan memperkenalkan ketidakakuratan dalam hasil akhir. Selain itu, contoh-contoh yang diberikan di sini tidak lengkap. Faktanya adalah bahwa fitur implementasi penerjemah JavaScript dapat, tanpa menyimpang dari spesifikasi, menyebabkan munculnya perbedaan dalam sebagian besar hasil numerik.

▍JavaScript


Saya tidak ingin mengkritik JavaScript. Saya percaya bahwa JavaScript membuat kompromi yang dapat dibenarkan, dengan mempertimbangkan ketidakpastian masa depan dan jumlah platform tempat implementasi bahasa dibuat. Saya harus mengatakan, sangat sulit untuk membandingkan secara langsung JavaScript dan bahasa lainnya. Intinya adalah ekosistem JavaScript. Fakta bahwa pada saat yang sama ada banyak penafsir dari bahasa yang sama sepenuhnya atipikal untuk bahasa lain. Dan ini, sebagai tambahan, adalah salah satu kekuatan utama dari JavaScript. Lebih jauh, seseorang tidak dapat gagal untuk mengatakan bahwa ini adalah fenomena dari rencana yang sama sekali berbeda dari yang terjadi dalam bahasa itu sendiri. Dan JavaScript berubah seiring waktu dan banyak hal baik muncul di dalamnya .

▍Stdlib atau epsilon?


Saya percaya bahwa dalam prakteknya dalam banyak kasus layak menggunakan pendekatan yang menyiratkan penggunaan nilai epsilon. Pustaka stdlib adalah alat yang sangat bagus, tetapi harga termasuk pustaka tambahan untuk perhitungan matematis dalam proyek bisa sangat tinggi. Dan dalam kebanyakan kasus, perbedaan kecil dalam hasil perhitungan tidak masalah untuk aplikasi.

Ringkasan


Di sini saya ingin menarik kesimpulan dari hal tersebut di atas dan berbagi beberapa pemikiran.

  1. , , , . . — « ». , , , Math.sin , , , . , , , , , . , , , .
  2. . , , Node.js, , , simply-statistics. , , , , . — .
  3. , . V8, , . , . , , , .

Pembaca yang budiman! Pernahkah Anda mengalami masalah terkait perubahan hasil perhitungan saat meningkatkan ke versi Node.js yang baru?


All Articles