حول الأساليب المتغيرة لكائن الرياضيات في جافا سكريبت

ننشر اليوم ترجمة لمقالة عن الحسابات الرياضية في جافا سكريبت ، وهي نسخة مكتوبة من عرض مؤلفها على WaffleJS . وهذا البيان نفسه كان قليلاً من استمرار هذه المحادثة على تويتر.


تعليم الرياضيات

الاهتمام بالرياضيات


بدأ كل شيء بتعليمي الرياضي ، مع تأخر استلامه إلى حد ما. عندما درست علوم الكمبيوتر ، كان بإمكاني التواصل مع معلمي الرياضيات على مستوى عالمي ، لكنني فوتت هذه الفرصة. لم يعجبني الرياضيات: كانت الموضوعات التي تمت دراستها بعيدة جدًا عن الممارسة. بالإضافة إلى ذلك ، كنت بالفعل غير متوازن من خلال برنامج نظري عميق لتدريب الكمبيوتر. ثم اعتقدت أنها انفصلت عن الحياة ، وفي الغالب ما زلت أعتقد ذلك.

ولكن الآن ، بعد بضع سنوات من إتمام دراستي ، استيقظ لي تعطش لتعلم الرياضيات. لقد استلهمت من التأثير الذي يمكن أن يحدثه حتى تطبيق صغير للمعرفة الرياضية على عملي وهوايتي. ولكن لم يكن لدي خطة دراسة واضحة.

ثم ، في عام 2012 ، وجدت طريقة لدراسة الرياضيات من خلال بدء العمل في مشروع الإحصاءات البسيطة . منذ ذلك الحين وسعت هذا المشروع ودعمته. الآن يتضمن تطبيق العديد من الخوارزميات ، وتبين أنها واحدة من أكثر مكتبات جافا سكريبت الرياضية " النجمية ". ويبدو أن الناس يستخدمون هذه المكتبة حقًا.

لكني بدأت العمل في عام 2012. إذا تحدثنا عن كيفية تغير التكنولوجيا خلال هذا الوقت ، فقد كان ذلك منذ وقت طويل جدًا. ومنذ ذلك الحين، 8 النشرات LTS من نود.جي إس قد أفرج عنهم . منذ ذلك الحين ، تغيرت JavaSript نفسها والبيئات التي تغيرت فيها البرامج المكتوبة بهذه اللغة كثيرًا. في عام 2012 ، لم تكن مكتبة React موجودة بعد ، ثم لم يتم الالتزام الأول بمشروع بابل بعد.


مرور الوقت

على مر السنين ، لاحظت أن اختباراتي تعطلت عند تحديث Node.js. على سبيل المثال ، قد يكون لدي شيء مثل هذا الاختبار:

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

يعمل هذا الاختبار بشكل جيد في Node.js v10 ، ولكنه ينكسر في Node.js v12. وهنا انها ليست بعض طريقة معقدة فائقة أن يتم اختبار: هو وظيفة gammaتنفيذها باستخدام وظائف جافا سكريبت القياسية - Math.pow، Math.sqrtو Math.sin.

علم الحساب


أعرف ما يمكنك التفكير فيه هنا: الحساب. على تويتر ، تشتعل المناقشات الساخنة بشكل دوري بسبب نتائج حساب التعبير التالي:

0.1 + 0.2 = 0.30000000000000004

ولكن ، كما كتبت بالفعل ، تتصرف جميع لغات البرمجة الشائعة بهذه الطريقة ، حتى اللغات القديمة والقديمة مثل هاسكل. قد تبدو عملية حساب الفاصلة العائمة غريبة ، لكنها تتصرف بنفس الطريقة ، وسلوكها موثق جيدًا. وبالتحديد ، نتحدث عن معيار IEEE 754 ، الذي يتم تطبيق متطلباته بدقة في لغات البرمجة. لذا ، فإن المشكلة ليست في الحساب: تنفيذ الجمع والطرح والقسمة والضرب في اللغات ، يمكن القول أن البرمجة "منحوتة في الحجر".

كائن رياضي


كانت مشكلتي في كائن JavaScript القياسي Math. على وجه الخصوص ، في جميع طرق هذا الكائن.

تقنيات مثل Math.sin، Math.cos، Math.exp، Math.pow، Math.tan- هي المكونات الأساسية للحسابات الهندسية والحسابات الأخرى. عندما فهمت هذا ، بدأت أدرس بشكل منفصل التغييرات في سلوك الطرق الأساسية للكائن Mathفي إصدارات مختلفة من Node.js. وهنا بعض الأمثلة.

الحساب Math.tanh(0.1):

// Node 4
0.09966799462495590234
// Node 6
0.09966799462495581907

الحساب Math.pow(1/3, 3):

// Node 10
0.03703703703703703498
// Node 12
0.03703703703703702804

والأسوأ من ذلك أن هذه المشكلة لا تظهر فقط في Node.js. يحدث الشيء نفسه في المتصفحات وفي البيئات الأخرى التي تدعم جافا سكريبت.

وهذا يقودنا إلى السؤال التالي: ما هو الحساب الرياضي؟


التمثيل البياني للحسابات

من السهل تصور الطرق المثلثية. إذا كان لديك دائرة واحدة وعدة أشهر من المدرسة الثانوية تحت تصرفك ، فأنت تعلم أن جيب التمام وجيب الزاوية هما إحداثيات نقطة معينة على حافة الدائرة ، وأن الرسوم البيانية للوظائف تخطئ و cos تبدو وكأنها موجات. في الواقع ، يدرسون في المدرسة الثانوية كيفية الحصول على هذه القيم ، ولكن الطريقة المستخدمة في ذلك - سلسلة تايلور - تعتمد على سلسلة لا حصر لها ، وليس من السهل على الكمبيوتر حل هذه المشاكل.

إليك ما يمكنك تعلمه من ويكيبيديافيما يتعلق بخوارزمية حساب الجيب: "لا توجد خوارزمية قياسية لحساب الجيب. لا يؤثر IEEE 754-2008 ، وهو المعيار الأكثر استخدامًا في حسابات الفاصلة العائمة ، على حساب الدوال المثلثية مثل الجيب. "

تستخدم أجهزة الكمبيوتر العديد من التقديرات والخوارزميات المختلفة لإجراء العمليات الحسابية ، مثل CORDIC ، وجميع أنواع الحيل الصعبة وجداول البحث. يفسر كل هذا عدم التجانس وجود العديد من مكتبات fastmath على GitHub . والحقيقة هي أن هناك العديد من الطرق لتنفيذ الطريقة Math.sin. ووظائف أخرى أيضًا. على سبيل المثال ، كما تعلم ، استخدمت ساحة Quake III بشكل أسرعاستبدال طريقة حساب الجذر التربيعي المعكوس القياسية لتسريع العرض.

ونتيجة لذلك ، فإن الحسابات الرياضية هي نتيجة تنفيذ خوارزميات معينة. من الناحية العملية ، يتم استخدام العديد من الخوارزميات الشائعة ومتغيراتها.

مواصفات JavaScript ، بدلاً من تحديد أي خوارزمية معينة لاستخدامها في تطبيقات اللغة ، تمنح التطبيقات مساحة كبيرة للمناورة من حيث الوظائف المستخدمة في الحسابات الرياضية.

إليك ما يقوله المعيار عن هذا (ECMA-262 ، الإصدار 10 ، القسم 20.2.2):

"سلوك الدوال acos ، acosh ، asin ، asinh ، atan ، atanh ، atan2 ، cbrt ، cos ، cosh ، exp ، expm1 ، hypot ، log ، log1p ، log2 ، log10 ، pow ، random ، sin ، sinh ، sqrt ، tan and tanh لم يتم وصفها بالكامل هنا ، باستثناء متطلبات إعادة نتائج معينة لقيم معينة من الحجج ، والتي تعتبر حالات حدودية جديرة بالملاحظة ".

لا أعرف كيف تعمل الأنشطة الداخلية لأعضاء اللجنة المسؤولة عن معيار ECMA-262 ، ولكن أعتقد أنهم جعلوا المعيار بحيث لا تكون هناك أزمة توافق في JavaScript إذا أصدرت Intel أو AMD تعليمات رياضية جديدة فائقة السرعة في معالجاتهم الطازجة.

نظرًا لوجود العديد من مترجمي جافا سكريبت على نطاق واسع ، نظرًا لأن جافا سكريبت غالبًا ما تستخدم في المتصفحات ، وبين المتصفحات لا يزال هناك شيء مثل المنافسة ، وبسبب حقيقة أن تطبيقات جافا سكريبت الشائعة تخضع لضغط مستمر وأجبرنا على التطور بسرعة ، لتقديم أفضل أداء ... بسبب كل هذا ، لدينا ما لدينا. سيختبر أي شخص يستخدم جافا سكريبت بانتظام حقيقة أن نتائج العمليات الحسابية التي يتم إجراؤها بوسائل الكائن Mathمختلفة في عمليات التنفيذ المختلفة.

هذا ليس مهمًا في اللغات المفسرة الأخرى ، نظرًا لأن لديهم عادةً بعض عمليات التنفيذ "الأساسية". على سبيل المثال ، هذا صحيح لمترجم بايثون.

أين تتم الحسابات؟


الآن دعونا نلقي نظرة فاحصة على مكان إجراء الحسابات بالضبط. في حالة جافا سكريبت ، هناك ثلاث مناطق يتم فيها إجراء العمليات الحسابية الأساسية:

  1. وحدة المعالجة المركزية.
  2. مترجم اللغة (C ++ و C code لتطبيق JavaScript محدد).
  3. كود JavaScript ، على سبيل المثال ، كود للمكتبات المتخصصة.

▍1. وحدة المعالجة المركزية


أول فكرة تبادر إلى ذهني عندما فكرت في الأماكن التي تتم فيها الحسابات هي أن الحسابات تتم في المعالج. اقترحت أنه بما أن المعالجات تنفذ الحسابات الحسابية ، فيمكنها أيضًا تنفيذ بعض العمليات الحسابية الأكثر تعقيدًا. اتضح أن المعالجات لديها تعليمات لأداء الحسابات المثلثية والحسابات الأخرى ، ولكن نادرًا ما يتم استخدام هذه التعليمات. على سبيل المثال ، لم يكن تطبيق حساب الجيب في المعالجات التي تحتوي على بنية x86 شائعًا جدًا ، لأن هذا التطبيق لا يتضح بالضرورة أنه أسرع من عمليات تنفيذ البرامج (تلك التي تستخدم العمليات الحسابية للمعالج). بالإضافة إلى ذلك ، ليس بالضرورة أكثر دقة من تطبيقات البرامج.

عانت Intel أيضًا من العار بسبب القوة الشديدةالمبالغة في تقدير دقة العمليات المثلثية في التوثيق. هذه الأخطاء مأساوية بشكل خاص بسبب حقيقة أنه لا يمكن تصحيح الرقاقة ، على عكس البرنامج.

▍2. مترجم لغة


هذه هي الطريقة التي تعمل بها أنظمة الحوسبة الفرعية في معظم تطبيقات JavaScript. ينفذون هذه النظم الفرعية بطرق مختلفة.

  • تستخدم محركات V8 و SpiderMonkey منافذ مكتبة fdlibm لإجراء العمليات الحسابية التي تختلف قليلاً. تم تسليم هذه المكتبة ، التي كُتبت أصلاً في Sun Microsystems ، من جيل إلى جيل.
  • يستخدم JavaScriptCore (Safari) مكتبة cmath لإجراء معظم العمليات.
  • يستخدم Internet Explorer كلاً من cmath وبعض كتل التعليمات البرمجية المكتوبة في أداة التجميع . هنا تم استخدام طرق المعالجات المثلثية - في حالة تجميع المتصفح للمعالجات التي لديها تعليمات مماثلة.

لأسباب تاريخية ، تم تغيير الأدوات المستخدمة لإجراء العمليات الحسابية في محركات JS مختلفة. لذا ، استخدم V8 حله الخاص للحسابات ، ثم تم استخدام منفذ جافا سكريبت fdlibm ، وبعد ذلك فقط تم استخدام الإصدار C من fdlibm.

▍ لماذا هذه مشكلة؟


النقطة هنا هي أن كل هذا يقلل من قدرة جافا سكريبت على إنتاج نتائج موحدة في حل أي مشاكل تتعلق بالحسابات الرياضية. هذا صعب بشكل خاص على علوم البيانات. أود أن تكون جافا سكريبت أكثر ملاءمة لإجراء عمليات حسابية لعلوم البيانات في المستعرض. علاوة على ذلك ، فإن عدم القدرة على تحقيق نتائج موحدة يعني تفاقم أزمة التكاثر ، التي تتميز بها جميع العلوم. ناهيك عن بعض مشكلات JavaScript الأخرى ، مثل ميزات كتابة الأرقام وعدم وجود مكتبة مستخدمة على نطاق واسع للعمل مع إطارات البيانات.

▍3. استخدام مكتبات متخصصة


هناك طريقة موثوقة لنا للقيام بالحسابات في JavaScript. يتكون من استخدام مكتبات متخصصة. لذا ، فإن مكتبة stdlib تنفذ حسابات عالية المستوى باستخدام العمليات الحسابية فقط. يتم وصف الحسابات الحسابية بالكامل في المواصفات ، فهي قياسية ، لذا فإن النتائج التي يتم إرجاعها بواسطة stdlib تعطينا ، بغض النظر عن النظام الأساسي الذي يتم تنفيذ الكود عليه ، نتائج موحدة تمامًا.

يتم تحقيق ذلك على حساب تعقيد وسرعة القرارات. طرق stdlib ليست سريعة مثل الطرق المضمنة. بالإضافة إلى ذلك ، من أجل "مجرد حساب الجيب" ، تحتاج إلى توصيل مكتبة كاملة بالمشروع.

ولكن ، إذا كنت تفكر على نطاق أوسع ، فهذا أمر طبيعي تمامًا. منصة WebAssembly ، على سبيل المثال ، لا تعطي المبرمج أي وسيلة لإجراء عمليات حسابية رياضية عالية المستوى. في الوثائق الخاصة به ، يوصى بأن تقوم بشكل مستقل بتضمين عمليات تنفيذ الآليات المقابلة في

وحداتك الخاصة: "لا تتضمن WebAssembly عمليات التنفيذ الخاصة بها للوظائف الرياضية - مثل الخطيئة ، cos ، exp ، pow ، وما إلى ذلك. تتمثل استراتيجية WebAssembly لمثل هذه الوظائف في السماح للمطورين بتنفيذها كأدوات مكتبة في منصة WebAssembly نفسها (لاحظ أن تعليمات x86 للنظام الأساسي و cos بطيئة وغير دقيقة ، وهذه الأيام على أي حال ، حاول ألا تستخدم). "

هذه هي الطريقة التي عملت بها اللغات المترجمة دائمًا: عند تجميع برنامج مكتوب بلغة C ، math.hيتم تضمين الطرق المستوردة من البرنامج المترجم.

باستخدام قيمة إبسيلون


إذا لم يرغب أحد في تضمين مكتبة stdlib في مشروع JavaScript الخاص به لإجراء العمليات الحسابية ، ولكنه يحتاج إلى اختبار الكود الذي يقوم ببعض العمليات الحسابية المعقدة ، فمن المحتمل أن يلجأ إلى الطريقة المستخدمة بالفعل في مكتبة الإحصائيات البسيطة. يتعلق الأمر باستخدام قيمة epsilonتحدد الحدود التي لا تؤخذ في الاعتبار الفروق في الأرقام. إذا أخذنا في الاعتبار خيارات استخدام رمز إبسيلون في الرياضيات ، فيمكننا القول إنني أتحدث عنه باعتباره "قيمة إيجابية صغيرة تعسفية". تستخدم الإحصائيات البسيطة قيمة إبسيلون المتساوية 0.0001.

إذا كنت بحاجة إلى معرفة ما إذا كان رقمان متساويين ، يتم التحقق من حالة النموذجMath.abs(result — expected) < epsilon. إذا تبين أن هذا الشرط صحيحًا ، فيمكننا القول أن الفرق بين الأرقام يقع ضمن النطاق المحدد ونعتبرها متساوية.

الإضافات


▍ الدقة


أشار المعلقون على تويتر إلى أن الاختلافات في النتائج التي تم الحصول عليها في المثال تقع خارج نطاق الأرقام المهمة لرقم النقطة العائمة. من وجهة نظر فنية ، هذا صحيح ، وهذا يعني أنه يمكنك العثور على طريقة أكثر دقة لمقارنة الأرقام من تلك التي تستخدم القيمة epsilon. ولكن من الناحية العملية ، نفس القصة هنا - تؤثر الأرقام في نهاية الرقم على النتيجة وتعطي عدم دقة في النتيجة النهائية. بالإضافة إلى ذلك ، فإن الأمثلة الواردة هنا ليست شاملة. والحقيقة هي أن ميزات التنفيذ لمترجمي جافا سكريبت يمكن أن تؤدي ، دون الخروج عن المواصفات ، إلى ظهور اختلافات في معظم النتائج العددية.

avaجافا سكريبت


لا أريد انتقاد جافا سكريبت. أعتقد أن جافا سكريبت قدمت حل وسط يمكن تبريره ، مع مراعاة عدم اليقين في المستقبل وعدد المنصات التي يتم إنشاء تطبيقات اللغة عليها. يجب أن أقول أنه من الصعب جدًا مقارنة جافا سكريبت بلغات أخرى مباشرة. النقطة هي نظام جافا سكريبت. حقيقة أنه في نفس الوقت هناك العديد من المترجمين من نفس اللغة أمر غير اعتيادي تمامًا للغات الأخرى. وهذا ، بالإضافة إلى ذلك ، أحد نقاط القوة الرئيسية لجافا سكريبت. علاوة على ذلك ، لا يمكن للمرء أن يفشل في القول إن هذه ظواهر لخطة مختلفة تمامًا عن تلك التي تحدث في اللغة نفسها. يتغير جافا سكريبت بمرور الوقت وتظهر الكثير من الأشياء الجيدة فيه .

tdStdlib أو إبسيلون؟


أعتقد أنه من الناحية العملية في معظم الحالات ، يجدر استخدام النهج الذي ينطوي على استخدام قيمة إبسيلون. تعد مكتبة stdlib أداة قوية رائعة ، ولكن سعر تضمين مكتبة إضافية للحسابات الرياضية في المشروع يمكن أن يكون مرتفعًا جدًا. وفي معظم الحالات ، لا يهم التناقضات الصغيرة في نتائج الحسابات للتطبيقات.

ملخص


أود هنا استخلاص استنتاجات مما سبق وتبادل بعض الأفكار.

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

القراء الأعزاء! هل واجهت مشاكل تتعلق بالتغييرات في نتائج الحساب عند الترقية إلى إصدارات جديدة من Node.js؟


All Articles