> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# Floating Point من الداخل: كيف يخزن الحاسب الكسور
- URL: https://arabroot.io/articles/الأرقام-العشرية-floating-point-من-الداخل/
- Published: 2026-10-08T18:00:00.000Z
- Updated: 2026-10-09T12:56:46.000Z
- Description: كيف يحفظ معيار IEEE 754 الكسور في البتات، ولماذا تكبر الفجوة بين الأرقام وتضيع خانات المعرفات، ومتى تحتاج النوع العشري.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, أساسيات الحاسب والبرمجة, DevOps وCI/CD, قواعد البيانات, الاستضافة الذاتية

في مقال [الأنظمة العددية للمبرمجين](https://arabroot.io/articles/%D8%A7%D9%84%D8%A7%D9%86%D8%B8%D9%85%D8%A9-%D8%A7%D9%84%D8%B9%D8%AF%D8%AF%D9%8A%D8%A9-%D9%88%D8%A7%D9%84%D8%AB%D9%86%D8%A7%D8%A6%D9%8A-%D9%84%D9%84%D9%85%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D9%86/#floating-point) رأينا أن `0.1 + 0.2` تعطي `0.30000000000000004`، وعرفنا أن السبب هو أن 0.1 كسر لا ينتهي بالنظام الثنائي، ثم قلنا في [قسم المبالغ المالية](https://arabroot.io/articles/%D8%A7%D9%84%D8%A7%D9%86%D8%B8%D9%85%D8%A9-%D8%A7%D9%84%D8%B9%D8%AF%D8%AF%D9%8A%D8%A9-%D9%88%D8%A7%D9%84%D8%AB%D9%86%D8%A7%D8%A6%D9%8A-%D9%84%D9%84%D9%85%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D9%86/#money) إن `double` لا يصلح للمال. ولكن هذه القاعدة وحدها لا تفسر لماذا يجمع برنامج الرقم 0.1 عشرة ملايين مرة فيعطي 1087937 بدلاً من مليون، ولا لماذا يصل معرف الطلب من ال API ناقصاً آخر خاناته، ولا لماذا يعطي `Math.Round(2.5)` في C# القيمة 2، ولا لماذا عجزت بطارية صواريخ Patriot في الظهران سنة 1991 عن اعتراض صاروخ لأن ساعتها تعد بأعشار الثانية. والإجابة في كل هذه الحالات موجودة في البتات نفسها، أي في الطريقة التي يقسم بها المعيار [IEEE 754](https://standards.ieee.org/ieee/754/6210/?ref=arabroot.io) الرقم إلى إشارة وأس وكسر، وما يترتب على هذا التقسيم من فجوات بين الأرقام وقيم خاصة وقواعد للتقريب.

وهذا المقال من مرحلة [أساسيات علوم الحاسب Computer Science Fundamentals](https://arabroot.io/start/#stage-foundations)، ويأتي بعد مقال الأنظمة العددية مباشرة، فإذا لم تقرأه فابدأ به لأننا لن نعيد شرح الثنائي والـ Hex والمتمم الثنائي هنا. وسوف نناقش في هذا المقال ما يلي:

- تشريح الرقم حسب IEEE 754 في `float` و`double`: الإشارة والأس مع الإزاحة Bias والكسر والبت المخفي، وفك بتات 0.1 و3.14 الحقيقية بيدك.
- كم رقماً عشرياً يحفظ كل نوع فعلاً، ولماذا تكبر المسافة بين الرقم والذي يليه ULP كلما كبر الرقم.
- القيم الخاصة: الصفر السالب واللانهاية و NaN والأرقام دون الطبيعية Subnormal، وكيف تتعامل معها كل لغة بطريقتها.
- التقريب Rounding إلى الزوجي، والمقارنة بقيمة سماح نسبية، وتراكم الخطأ وجمع Kahan مع قياس حقيقي.
- الأرقام الصحيحة الكبيرة داخل `double` ومعرفات JSON، والأنواع العشرية في اللغات وفي PostgreSQL.
- الأنواع الصغيرة في الذكاء الاصطناعي: fp16 وbfloat16 وfp8، ثم حادثتان موثقتان: Patriot وAriane 5.
- تجربة عملية كاملة في Containers بلغة C# وPython وJavaScript (Node.js) وقاعدة PostgreSQL، مع المخرجات الحقيقية.

## تشريح الرقم: الإشارة والأس والكسر

الفكرة التي ذكرناها في مقال الأنظمة العددية هي أن الرقم يكتب بصيغة علمية بالأساس 2، والآن سوف ننزل إلى التفاصيل، فكل رقم طبيعي Normal في IEEE 754 قيمته تساوي:

```text
value = (-1)^sign × 1.fraction × 2^(exponent - bias)
```

والجدول التالي يبين كيف يقسم المعيار البتات في النوعين الأشهر، وهما `float` بحجم 32 بت، ويسميه المعيار binary32، و`double` بحجم 64 بت، ويسميه binary64، وهو النوع الافتراضي للأرقام العشرية في C# وJava وPython، والنوع الوحيد للأرقام في JavaScript:

| النوع             | الإشارة Sign | الأس Exponent | الإزاحة Bias | الكسر Fraction | الدقة الفعلية |
| ----------------- | ------------ | ------------- | ------------ | -------------- | ------------- |
| float (binary32)  | 1 بت         | 8 بتات        | 127          | 23 بتاً        | 24 بتاً       |
| double (binary64) | 1 بت         | 11 بتاً       | 1023         | 52 بتاً        | 53 بتاً       |

وفي هذا الجدول شيئان يحتاجان إلى شرح. الأول هو البت المخفي Hidden Bit، ويسمى أيضاً الواحد الضمني Implicit Leading 1، فكل رقم طبيعي بالثنائي إذا كتبته بالصيغة العلمية يبدأ بالواحد قبل الفاصلة، تماماً كما أن الرقم العشري بالصيغة العلمية يبدأ برقم من 1 إلى 9، والفرق أن الثنائي ليس فيه إلا رقم واحد غير الصفر، وهو الواحد، ولذلك لا داعي لحفظه أصلاً، فيخزن المعيار ما بعد الفاصلة فقط ويضيف الواحد عند القراءة، وبهذا يكسب بتاً إضافياً من الدقة مجاناً، فتصبح دقة `double` الفعلية 53 بتاً مع أن الكسر 52 بتاً فقط.

والثاني هو الإزاحة Bias، وقد يتساءل البعض: لماذا يخزن الأس بعد إضافة 1023 إليه بدلاً من استخدام المتمم الثنائي Two's Complement الذي شرحناه في [قسم الأرقام السالبة](https://arabroot.io/articles/%D8%A7%D9%84%D8%A7%D9%86%D8%B8%D9%85%D8%A9-%D8%A7%D9%84%D8%B9%D8%AF%D8%AF%D9%8A%D8%A9-%D9%88%D8%A7%D9%84%D8%AB%D9%86%D8%A7%D8%A6%D9%8A-%D9%84%D9%84%D9%85%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D9%86/#signed)؟ والإجابة أن الإزاحة تجعل الأس المخزن رقماً موجباً دائماً، وبالتالي إذا وضعت الأس قبل الكسر وقرأت بتات رقمين موجبين كأنهما عددان صحيحان، فإن ترتيبهما كأعداد صحيحة هو نفس ترتيبهما كأرقام عشرية، وهذا يجعل المقارنة والترتيب أبسط في العتاد Hardware، وهو أيضاً السبب في أن الرقم الذي يلي أي رقم موجب هو ببساطة بتاته زائد واحد، كما سوف نرى في تجربة Node.js.

### مثال: نفك بتات 3.14 بيدنا

لنأخذ الرقم 3.14، فقد طبعت بتاته البرامج الثلاثة في [التجربة العملية](#lab) بنفس القيمة `0x40091EB851EB851F`، وطريقة فكها كما يلي:

1. نحول الـ Hex إلى ثنائي ونقسمه 1 و11 و52، فالإشارة `0`، أي أن الرقم موجب.
2. الأس المخزن `10000000000` وقيمته 1024، فنطرح الإزاحة 1024 - 1023 ويكون الأس الحقيقي 1.
3. الكسر `1001000111101011100001010001111010111000010100011111`، فنضع قبله البت المخفي فيصبح `1.1001000111101011100001010001111…`، وقيمته بالعشري 1.5700000000000000621724893790087662637233734130859375.
4. نضرب في 21 فيكون الناتج 3.140000000000000124344978758017532527446746826171875، وهذه بالضبط القيمة التي طبعتها `Decimal(3.14)` في Python.

لاحظ أن الكسر ينتهي بالبتات `…0001111` ثم يقطع، والسبب أن 0.14 بالثنائي مثل 0.1 كسر لا ينتهي، فقرب المعيار آخر بت إلى أقرب قيمة، وكانت هذه القيمة أكبر قليلاً من 3.14 الحقيقية. أما في `float` فبتات 3.14 هي `0x4048F5C3`، والأس المخزن 128 - 127 = 1 أيضاً، ولكن الكسر 23 بتاً فقط، فالقيمة المخزنة 3.1400001049041748046875، أي أن الخطأ ظهر في الخانة السابعة بدلاً من الخانة السابعة عشرة. والصورة التالية تبين البتات الحقيقية للرقم 3.14 في النوعين وكيف تتحول إلى القيمة:

![64 خانة لبتات 3.14 من النوع double بالقيمة 0x40091EB851EB851F مقسمة إلى بت إشارة بنفسجي و11 بتاً للأس بالبرتقالي و52 بتاً للكسر بالأزرق، وتحتها خطوات الفك: الأس 1024 ناقص 1023 يساوي 1، والبت المخفي 1 قبل الكسر، والناتج 3.14000000000000012، ثم 32 خانة للنوع float بالقيمة 0x4048F5C3 وقيمته 3.14000010](https://arabroot.io/content/images/2026/10/floating-point-01-layout.webp)

البت المخفي لا يخزن، والأس يخزن بعد إضافة الإزاحة، والكسر يقطع عند آخر بت متاح

## كم رقماً عشرياً يحفظ float وdouble؟

الدقة في IEEE 754 تقاس بالبتات وليس بالخانات العشرية، فـ `float` يحفظ 24 بتاً من الأرقام المهمة، و224 يساوي 16,777,216، أي أكثر قليلاً من سبع خانات عشرية، و`double` يحفظ 53 بتاً، و253 يساوي تقريباً 9 × 1015، أي قرابة 16 خانة. ولهذا تجد في [جدول أنواع C#](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/floating-point-numeric-types?ref=arabroot.io) أن دقة `float` من 6 إلى 9 خانات، ودقة `double` من 15 إلى 17، وهذان الرقمان لهما معنيان مختلفان:

- **الرقم الأصغر (6 و15):** هو عدد الخانات العشرية الذي تضمن أن يعود كما هو إذا كتبته كنص ثم حولته إلى الرقم ثم أعدته نصاً، ولهذا تطبع Python القيمة `sys.float_info.dig = 15`، وتقول [وثائق PostgreSQL](https://www.postgresql.org/docs/current/datatype-numeric.html?ref=arabroot.io) إن دقة `real` ست خانات على الأقل ودقة `double precision` خمس عشرة خانة على الأقل.
- **الرقم الأكبر (9 و17):** هو عدد الخانات الذي تحتاجه لتطبع الرقم المخزن بحيث لا يختلط بجاره، ولهذا طبعت Python الرقم 0.1 من النوع `float` بسبع عشرة خانة فظهر `0.10000000149011612`.

وبعد هذه الحدود يبدأ الرقم في فقدان خاناته بصمت، فالرقم 16777217 أصبح 16777216 في `float`، والرقم 9007199254740993 أصبح 9007199254740992 في `double`، ولم تظهر أي رسالة خطأ في الحالتين. وفي PostgreSQL ظهرت مفاجأة إضافية، فالاستعلام `SELECT 0.1::real` يعرض 0.1، بينما تحويل نفس القيمة إلى `float8` يعرض 0.10000000149011612، والسبب أن PostgreSQL يعرض أقصر نص يعيد نفس القيمة، أما تحويل `16777217::real` إلى `numeric` فأعطى 16777200، أي أن التحويل احتفظ بست خانات فقط، وهذا مثال واضح على أن الرقم الذي تراه على الشاشة ليس دائماً الرقم المخزن.

والقاعدة العملية: اجعل `double` هو الاختيار الافتراضي، واستخدم `float` فقط عندما تكون الأرقام تقريبية أصلاً وكثيرة جداً، مثل الرسوميات وقراءات الحساسات وأوزان نماذج التعلم الآلي، والسبب أن `float` يأخذ نصف ذاكرة `double` كما شرحنا في [مقال الذاكرة](https://arabroot.io/articles/%D8%A7%D9%84%D8%B0%D8%A7%D9%83%D8%B1%D8%A9-%D9%84%D9%84%D9%85%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D9%86-stack-%D9%88-heap-%D9%88%D8%A7%D9%84%D9%85%D8%A4%D8%B4%D8%B1%D8%A7%D8%AA/)، ويتسع منه في مسجل SIMD بحجم 256 بت ثمانية أرقام بدلاً من أربعة، كما رأينا في [مقال المعالج](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D9%8A%D8%B9%D9%85%D9%84-%D8%A7%D9%84%D9%85%D8%B9%D8%A7%D9%84%D8%AC-cpu-%D9%85%D9%86-%D8%A7%D9%84%D8%AF%D8%A7%D8%AE%D9%84/#simd)، ولكن مقابل ذلك تخسر نصف الخانات تقريباً.

## المسافة بين الرقم والذي يليه تكبر مع حجمه

من القسم السابق ينتج أمر مهم، وهو أن الأرقام العشرية في الحاسب ليست موزعة بالتساوي على خط الأعداد، فبين 1 و2 يوجد 252 رقماً في `double`، وبين 2 و4 يوجد نفس العدد بالضبط، وبين 1024 و2048 كذلك، لأن الأس يحدد المجال والكسر يقسمه إلى نفس عدد الخطوات دائماً، وبالتالي كلما تضاعف حجم الرقم تضاعفت المسافة بينه وبين الرقم الذي يليه. وهذه المسافة تسمى ULP، اختصاراً لـ Unit in the Last Place، أي قيمة آخر بت في الكسر عند هذا الحجم. والصورة التالية تبين ذلك:

![خط أعداد مقسم إلى مجالات من 1 إلى 2 ومن 2 إلى 4 ومن 4 إلى 8 ومن 8 إلى 16، في كل مجال نفس عدد العلامات فتتباعد العلامات كلما اتجهنا يميناً، وتحته جدول بالمسافة الحقيقية بين الرقم والذي يليه في double من math.ulp: عند 1 تساوي 2.2e-16 وعند مليون 1.2e-10 وعند 2 أس 53 تساوي 2، وفي float عند مليون تساوي 0.0625](https://arabroot.io/content/images/2026/10/floating-point-02-ulp.webp)

نفس عدد الأرقام في كل مجال، فتتضاعف المسافة بينها كلما تضاعف المجال

وفي Python تحسب هذه المسافة بالدالة [math.ulp](https://docs.python.org/3/library/math.html?ref=arabroot.io#math.ulp)، وتحصل على الرقم التالي بالدالة `math.nextafter`، وفي C# بالدالة [Math.BitIncrement](https://learn.microsoft.com/en-us/dotnet/api/system.math.bitincrement?ref=arabroot.io)، وفي Java بالدالتين `Math.ulp` و`Math.nextUp` في [الكلاس Math](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/Math.html?ref=arabroot.io)، أما JavaScript فليس فيها دالة جاهزة، فكتبنا في التجربة دالة تزيد بتات الرقم بمقدار واحد، وهذه بعض القيم الحقيقية من المخرجات:

| الرقم                                            | المسافة إلى الرقم التالي في double | المسافة في float |
| ------------------------------------------------ | ---------------------------------- | ---------------- |
| 1                                                | 2.22 × 10\-16                      | 1.19 × 10\-7     |
| 1,000,000                                        | 1.16 × 10\-10                      | 0.0625           |
| 1,000,000,000                                    | 1.19 × 10\-7                       | لم نقسها         |
| 1,791,460,000 (وقت Unix بالثواني في أكتوبر 2026) | 2.38 × 10\-7 ثانية                 | 128 ثانية        |
| 253 و1016                                        | 2                                  |                  |
| 10300                                            | 1.49 × 10284                       |                  |

وفي الجدول أعلاه لاحظ التالي:

- عند 1016 أصبحت المسافة 2، ولهذا طبعت Python أن `1e16 + 1 == 1e16` صحيح، فالرقم 1 أصغر من نصف المسافة، فيقرب الناتج إلى نفس الرقم ويضيع الجمع كله.
- في `float` عند مليون المسافة 0.0625، فإذا أضفت 0.1 إلى مليون لا يمكن أن يزيد الرقم 0.1، وإنما يقرب إلى أقرب مضاعف للمسافة، ولهذا طبعت C# أن `(1e6f + 0.1f) - 1e6f` يساوي 0.125، وهذا هو سر الرقم 1087937 الذي سوف نراه في [قسم تراكم الخطأ](#summation).
- وقت Unix الحالي في `float` يحفظ بفجوة 128 ثانية، فالرقم 1791460000 خزن كـ 1791459968، لذلك لا تحفظ الوقت أبداً في `float`، واحفظه كعدد صحيح 64 بت كما شرحنا في [مقال التاريخ والوقت](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%A7%D8%B1%D9%8A%D8%AE-%D9%88%D8%A7%D9%84%D9%88%D9%82%D8%AA-utc-%D9%88%D8%A7%D9%84%D9%85%D9%86%D8%A7%D8%B7%D9%82-%D8%A7%D9%84%D8%B2%D9%85%D9%86%D9%8A%D8%A9-%D9%88%D8%A7%D9%84%D8%AA%D9%82%D9%88%D9%8A%D9%85-%D8%A7%D9%84%D9%87%D8%AC%D8%B1%D9%8A/#unix-time).

⚠️

في C# لا تستخدم `double.Epsilon` كقيمة سماح في المقارنة، فهو حسب [توثيقه](https://learn.microsoft.com/en-us/dotnet/api/system.double.epsilon?ref=arabroot.io) أصغر رقم موجب أكبر من الصفر، أي 5E-324 كما طبعته التجربة، وهو رقم دون طبيعي Subnormal وليس إبسلون الآلة Machine Epsilon، والمقارنة به تساوي تقريباً المقارنة بـ ==. أما إبسلون الآلة، أي المسافة بين 1 والرقم التالي، فهو 2.220446049250313E-16، ويسمى [Number.EPSILON](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global%5FObjects/Number/EPSILON?ref=arabroot.io) في JavaScript و`sys.float_info.epsilon` في Python.

## القيم الخاصة: صفران ولا نهاية و NaN

حجز المعيار قيمتين من قيم الأس لحالات خاصة، فإذا كانت كل بتات الأس أصفاراً فالرقم صفر أو رقم دون طبيعي، وإذا كانت كلها آحاداً فالرقم لا نهاية Infinity أو ليس رقماً NaN، اختصاراً لـ Not a Number. والصورة التالية تبين بتات كل حالة في `double`:

![ستة صفوف من البتات: الصفر الموجب كل بتاته أصفار، والصفر السالب بت الإشارة فقط واحد، واللانهاية الأس كله آحاد والكسر أصفار، و NaN الأس كله آحاد والكسر ليس صفراً، والرقم دون الطبيعي الأس أصفار والكسر ليس صفراً وأصغره 5e-324، وأصغر رقم طبيعي 2.2e-308](https://arabroot.io/content/images/2026/10/floating-point-03-special.webp)

قيمتا الأس الأصغر والأكبر محجوزتان للصفر والأرقام دون الطبيعية واللانهاية و NaN

وكل قيمة من هذه القيم لها سلوك يفاجئ المطور عندما يقابله لأول مرة، وهذا ما ظهر في التجربة:

- **الصفر السالب Negative Zero:** بما أن الإشارة بت مستقل، فهناك صفر موجب وصفر سالب، وهما متساويان في المقارنة، فـ `0.0 == -0.0` صحيح في كل اللغات، ولكن القسمة عليهما مختلفة، فـ `1 / -0` في JavaScript يعطي `-Infinity`، ولا يفرق بينهما في JavaScript إلا [Object.is](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global%5FObjects/Object/is?ref=arabroot.io). ويظهر الصفر السالب عادة عندما تقرب رقماً سالباً صغيراً، فـ `Math.round(-0.5)` أعطى `-0`، وقد يظهر في التقارير كـ `-0` فيسأل عنه المستخدم.
- **اللانهاية Infinity:** ناتج الفيضان، فـ `1e308 * 10` يعطي `inf` بدلاً من رمي خطأ، و`1.0 / 0.0` يعطي `Infinity` في C# وJavaScript، أما Python فاختارت أن ترمي `ZeroDivisionError`، وهذا اختيار اللغة وليس المعيار.
- **NaN:** ناتج العمليات التي ليس لها جواب، مثل `inf - inf` و`0.0 / 0.0` و`Math.Sqrt(-1)`. والقاعدة الأغرب في المعيار أن NaN لا يساوي أي شيء ولا حتى نفسه، فـ `NaN == NaN` خطأ في اللغات الثلاث، وتحقق منه بـ `math.isnan` أو `double.IsNaN` أو `Number.isNaN`.
- **الأرقام دون الطبيعية Subnormal:** عندما يصبح الأس أصغر من أقل قيمة طبيعية، وهي 2.2250738585072014e-308، يتوقف المعيار عن إضافة البت المخفي، فتصغر الأرقام تدريجياً مع فقدان الدقة حتى أصغر رقم ممكن 5e-324، وهذا يسمى التناقص التدريجي Gradual Underflow، ونصف هذا الرقم يقرب إلى صفر كما طبعت Python و`Number.MIN_VALUE / 2` في Node.js.

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

| البيئة     | السلوك                                             | الناتج                                                |
| ---------- | -------------------------------------------------- | ----------------------------------------------------- |
| Python     | nan in \[nan\] مع نفس الكائن، ثم مع كائنين مختلفين | True ثم False، لأن البحث يقارن هوية الكائن قبل القيمة |
| Python     | max(nan, 1) وmax(1, nan)                           | nan و1، فالنتيجة تعتمد على الترتيب                    |
| Python     | sorted(\[3, nan, 1, 2\])                           | \[3, nan, 1, 2\]، أي أن الترتيب فشل بصمت              |
| JavaScript | \[NaN\].indexOf(NaN) و\[NaN\].includes(NaN)        | \-1 وtrue                                             |
| JavaScript | JSON.stringify(\[NaN, Infinity, -0\])              | \[null,null,0\]                                       |
| Python     | json.dumps(nan)                                    | NaN، وهو نص ليس JSON صحيحاً                           |
| C#         | nan == double.NaN وnan.Equals(nan)                 | False وTrue                                           |
| C#         | Array.Sort على مصفوفة فيها NaN                     | \[NaN, 1, 2, 3\]                                      |
| PostgreSQL | 'NaN'::float8 = 'NaN'::float8                      | t، وNaN أكبر من Infinity                              |

والسبب في سلوك PostgreSQL مذكور صراحة في [وثائق الأنواع الرقمية](https://www.postgresql.org/docs/current/datatype-numeric.html?ref=arabroot.io): المعيار يقول إن NaN لا يساوي شيئاً، ولكن قاعدة البيانات تحتاج أن ترتب القيم وتبني عليها فهارس Indexes، لذلك تعامل NaN كقيمة تساوي نفسها وأكبر من كل الأرقام. وهذا يعني أن نفس البيانات قد تعطي نتيجة مختلفة إذا قارنتها في الكود وإذا قارنتها في الاستعلام، فلا تسمح لـ NaN بالدخول إلى بياناتك أصلاً، وتحقق منه عند الإدخال. أما `json.dumps` في Python فيكتب `NaN` افتراضياً مع أن صيغة JSON في [RFC 8259](https://www.rfc-editor.org/rfc/rfc8259?ref=arabroot.io#section-6) لا تسمح به، فإذا قرأ الملف برنامج آخر فسوف يفشل، لذلك مرر `allow_nan=False` كما في [توثيق الوحدة json](https://docs.python.org/3/library/json.html?ref=arabroot.io) حتى ترمي الدالة خطأ واضحاً بدلاً من كتابة ملف لا يقرأ.

## التقريب: لماذا يعطي Math.Round(2.5) القيمة 2؟

كل عملية على الأرقام العشرية تنتهي بتقريب، لأن الناتج الحقيقي لجمع أو ضرب رقمين نادراً ما يكون له تمثيل دقيق، فيختار المعالج أقرب رقم ممكن. والطريقة الافتراضية في IEEE 754 هي التقريب إلى الأقرب، وعند التعادل إلى الزوجي Round Half to Even، أي إذا كان الناتج في المنتصف تماماً بين رقمين، اختر الرقم الذي ينتهي آخر بت فيه بصفر. ونفس القاعدة تستخدمها بعض دوال التقريب في اللغات، ويسميها البعض تقريب البنوك Banker's Rounding:

- في Python أعطت `round(0.5), round(1.5), round(2.5), round(3.5)` القيم 0 و2 و2 و4، و[توثيق round](https://docs.python.org/3/library/functions.html?ref=arabroot.io#round) يقول إن التقريب يتجه إلى الخيار الزوجي.
- في C# أعطى `Math.Round(2.5)` القيمة 2، لأن الوضع الافتراضي في [Math.Round](https://learn.microsoft.com/en-us/dotnet/api/system.math.round?ref=arabroot.io) هو `MidpointRounding.ToEven`، وحتى `(2.5).ToString("F0")` أعطى 2.
- في PostgreSQL أعطى `round(2.5::float8)` القيمة 2، بينما أعطى `round(2.5::numeric)` القيمة 3، و[توثيق الدوال الرياضية](https://www.postgresql.org/docs/current/functions-math.html?ref=arabroot.io) يقول إن `numeric` يقرب التعادل بعيداً عن الصفر، أما `double precision` فيعتمد على النظام والأشهر هو التقريب إلى الزوجي.
- أما JavaScript فتختلف عن الجميع، فـ [Math.round](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global%5FObjects/Math/round?ref=arabroot.io) يقرب التعادل نحو الموجب، فأعطى `Math.round(2.5)` القيمة 3 و`Math.round(-2.5)` القيمة `-2`.

وقد يتساءل البعض: لماذا يختار أحد التقريب إلى الزوجي وهو يخالف ما تعلمناه في المدرسة؟ والإجابة أن التقريب الذي تعلمناه يرفع كل حالة تعادل إلى الأعلى، فإذا قربت الأرقام 0.5 و1.5 و2.5 و3.5 بالطريقة المدرسية فمجموعها بعد التقريب 1 + 2 + 3 + 4 = 10، بينما مجموعها الحقيقي 8، أما التقريب إلى الزوجي فيعطي 0 + 2 + 2 + 4 = 8، لأن نصف حالات التعادل تنزل ونصفها يصعد، فلا يتراكم انحياز Bias في اتجاه واحد عندما تجمع آلاف القيم المقربة. ولهذا يستخدمه المعيار داخل كل عملية حسابية.

وهناك فخ ثان أخطر، وهو أن الرقم الذي تظن أنه في المنتصف ليس في المنتصف أصلاً، فقد رأينا في [مقال الأنظمة العددية](https://arabroot.io/articles/%D8%A7%D9%84%D8%A7%D9%86%D8%B8%D9%85%D8%A9-%D8%A7%D9%84%D8%B9%D8%AF%D8%AF%D9%8A%D8%A9-%D9%88%D8%A7%D9%84%D8%AB%D9%86%D8%A7%D8%A6%D9%8A-%D9%84%D9%84%D9%85%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D9%86/#money) أن 2.675 مخزنة كـ 2.67499999999999982236431605997495353221893310546875، فأعطت `round(2.675, 2)` في Python القيمة 2.67\. والجديد هنا أن C# أعطت لنفس الرقم `Math.Round(2.675, 2, MidpointRounding.AwayFromZero)` القيمة 2.68، والسبب أن خوارزمية التقريب إلى خانتين تختلف بين اللغتين، فالأولى تعمل على القيمة المخزنة بالضبط والثانية تضرب في 100 أولاً، وهذا وحده كاف لتعرف أن تقريب `double` إلى خانات عشرية لا يعطي نتيجة يمكن الاعتماد عليها بين نظامين. والحل أن تقرب بنوع عشري وتحدد الطريقة صراحة، فـ `Decimal("2.675").quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)` في Python و`Math.Round(2.675m, 2, MidpointRounding.AwayFromZero)` في C# و`round(2.675::numeric, 2)` في PostgreSQL أعطت كلها 2.68، وفي Java تحدد الطريقة بـ [RoundingMode](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/math/RoundingMode.html?ref=arabroot.io) مثل `HALF_UP` أو `HALF_EVEN`. واختر الطريقة التي يطلبها النظام المحاسبي أو الضريبي الذي تعمل له، ولا تتركها للقيمة الافتراضية للغة.

## كيف تقارن رقمين عشريين؟

ذكرنا في مقال الأنظمة العددية أن المقارنة بـ == لا تصلح، والآن سوف نرى لماذا لا تصلح أيضاً قيمة السماح الثابتة التي يستخدمها أغلب المطورين. فالحل البسيط هو `abs(a - b) < 1e-9`، وهو يعمل مع `0.1 + 0.2` و`0.3` لأن الفرق بينهما 5.551115123125783e-17 كما طبعته Python، ولكنه يفشل في الاتجاهين:

- **مع الأرقام الكبيرة:** الرقمان `1e15 + 0.3` و`1e15 + 0.5` متجاوران تقريباً عند هذا الحجم، لأن المسافة بين الأرقام هناك 0.125، ومع ذلك أعطت المقارنة بقيمة السماح الثابتة `False`، فقيمة 1e-9 أصغر من المسافة نفسها، وبالتالي فهي تساوي == عند هذا الحجم.
- **مع الأرقام الصغيرة جداً:** إذا كانت أرقامك قياسات بحجم 10\-10، فقيمة السماح 1e-9 أكبر من الأرقام نفسها، فتعتبر كل القياسات مساوية للصفر.

والطريقة الصحيحة هي قيمة سماح نسبية Relative Tolerance، أي أن يكون الفرق صغيراً بالنسبة لحجم الرقمين، مع قيمة سماح مطلقة Absolute Tolerance للأرقام القريبة من الصفر فقط. وهذا ما تفعله [math.isclose](https://docs.python.org/3/library/math.html?ref=arabroot.io#math.isclose) في Python، فهي تعتبر الرقمين متساويين إذا كان `abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)`، والقيمة الافتراضية `rel_tol=1e-09` و`abs_tol=0.0`. ولاحظ النتائج في التجربة: `isclose` أعطت `True` للرقمين الكبيرين، وأعطت `False` عند مقارنة 1e-10 بالصفر، لأن أي نسبة من الصفر تساوي صفراً، فلما أضفنا `abs_tol=1e-9` أصبحت `True`. وفي C# وJavaScript لا توجد دالة جاهزة، فاكتب نفس المعادلة في دالة مساعدة واحدة في مشروعك، واختر قيمة السماح من طبيعة المسألة، فدقة الحساس أو أصغر وحدة في العملة هي التي تحدد متى يكون الرقمان «متساويين»، وليس رقماً تنسخه من الإنترنت.

## تراكم الخطأ: جمع 0.1 عشرة ملايين مرة

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

| البيئة     | الطريقة                             | الناتج            | الوقت  |
| ---------- | ----------------------------------- | ----------------- | ------ |
| C#         | double بالجمع العادي                | 999999.9998389754 | 22 ms  |
| C#         | double مع Kahan                     | 1000000           | 81 ms  |
| C#         | decimal                             | 1000000.0         | 196 ms |
| C#         | float بالجمع العادي                 | 1087937           | 22 ms  |
| Node.js    | Number بالجمع العادي                | 999999.9998389754 | 41 ms  |
| Node.js    | Number مع Kahan                     | 1000000           | 59 ms  |
| Node.js    | عدد صحيح بالهللات ثم القسمة على 100 | 1000000           | 19 ms  |
| Node.js    | float32 عبر Math.fround             | 1087937           | 39 ms  |
| Python     | float بالجمع العادي                 | 999999.9998389754 | 3.09 s |
| Python     | Kahan                               | 1000000.0         | 6.37 s |
| Python     | math.fsum                           | 1000000.0         | 0.78 s |
| Python     | Decimal                             | 1000000.0         | 7.35 s |
| PostgreSQL | sum على عمود float8                 | 999999.9998389754 | 2.2 s  |
| PostgreSQL | sum على عمود real                   | 1.087937e+06      | 1.7 s  |
| PostgreSQL | sum على عمود numeric(12,2)          | 1000000.00        | 2.1 s  |

والأوقات في الجدول للتوضيح فقط، فقد قسناها مرة واحدة على جهاز تطوير وهي تتغير بين تشغيل وآخر، فوقت Kahan في C# تراوح بين 81 و204 ms في ثلاثة تشغيلات، وأوقات PostgreSQL الثلاثة تراوحت بين 1.1 و4 ثوان في تشغيلات متتالية، لأن قراءة عشرة ملايين صف تأخذ أغلب الوقت. أما النتائج نفسها فثابتة ومتطابقة، وفي الجدول أعلاه لاحظ التالي:

- نتيجة `double` هي 999999.9998389754 في البيئات الأربع حرفاً بحرف، لأنها كلها تطبق نفس المعيار بنفس طريقة التقريب، فالخطأ ليس في لغة معينة.
- نتيجة `float` أكبر من مليون بـ 87937، والسبب ما رأيناه في قسم المسافة بين الأرقام، فعندما يقترب المجموع من مليون تصبح المسافة 0.0625، فيقرب كل 0.1 إلى 0.125، فيزيد المجموع أكثر مما يجب في كل خطوة.
- جمع Kahan أعطى الناتج الصحيح بنفس النوع `double`، ودفعنا مقابله وقتاً أطول، أما `math.fsum` في Python فكان أسرع من الجمع العادي نفسه لأنه مكتوب بلغة C، وتصفه [الوثائق](https://docs.python.org/3/library/math.html?ref=arabroot.io#math.fsum) بأنه يتتبع عدة مجاميع جزئية حتى لا يفقد الدقة.
- العدد الصحيح بالهللات هو الأسرع والأدق معاً، وهذا يؤكد ما قلناه في [قسم المبالغ المالية](https://arabroot.io/articles/%D8%A7%D9%84%D8%A7%D9%86%D8%B8%D9%85%D8%A9-%D8%A7%D9%84%D8%B9%D8%AF%D8%AF%D9%8A%D8%A9-%D9%88%D8%A7%D9%84%D8%AB%D9%86%D8%A7%D8%A6%D9%8A-%D9%84%D9%84%D9%85%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D9%86/#money).

### كيف يعمل جمع Kahan؟

الفكرة التي نشرها William Kahan، ويشرحها David Goldberg في ورقته المشهورة [What Every Computer Scientist Should Know About Floating-Point Arithmetic](https://docs.oracle.com/cd/E19957-01/806-3568/ncg%5Fgoldberg.html?ref=arabroot.io)، أن الجزء الذي يضيع من الرقم الصغير عند جمعه مع المجموع الكبير يمكن حسابه، فنحفظه في متغير تعويض Compensation ونضيفه في الخطوة التالية. والخطوات في كل دورة أربع:

1. `y = x - c`: نطرح من القيمة الجديدة ما ضاع في الخطوة السابقة.
2. `t = sum + y`: نجمع، وهنا تضيع البتات المنخفضة من `y` لأن `sum` كبير.
3. `c = (t - sum) - y`: الفرق `t - sum` هو ما أضيف فعلاً، وطرح `y` منه يعطي ما ضاع بالسالب.
4. `sum = t`: ونكمل.

![مقارنة بين حلقتين: في الجمع العادي يدخل 0.1 إلى المجموع الكبير فيسقط جزء صغير منه في سلة المهملات في كل دورة، وفي جمع Kahan يحفظ الجزء الساقط في متغير c ويعاد إلى الدورة التالية، مع الناتجين الحقيقيين 999999.9998389754 و1000000](https://arabroot.io/content/images/2026/10/floating-point-04-kahan.webp)

جمع Kahan يحسب الجزء الذي ضاع في كل عملية ويعيده في العملية التالية

ويجدر الانتباه إلى أن هذه الخطوات تبدو للمترجم Compiler كأنها لا تفعل شيئاً، لأن `(t - sum) - y` يساوي صفراً في الرياضيات العادية، وتذكر ورقة Goldberg أن المترجم الذي يعيد ترتيب العمليات كأنها أرقام حقيقية قد يحذف التعويض كله، لذلك إذا استخدمت Kahan في C أو C++ فلا تفعل على هذا الكود خيارات المترجم التي تسمح بإعادة ترتيب العمليات العشرية، مثل `-ffast-math` في [GCC](https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html?ref=arabroot.io). وفي C# وJavaScript وPython لم يحدث ذلك، والدليل أن الناتج كان مليوناً بالضبط.

## الأرقام الصحيحة الكبيرة في double ومعرفات JSON

الكسر في `double` مع البت المخفي 53 بتاً، لذلك يمثل كل الأرقام الصحيحة حتى 253 بالضبط، وبعدها تصبح المسافة بين الأرقام 2 كما رأينا، فتختفي الأرقام الفردية، ثم تصبح 4 بعد 254، وهكذا. ولهذا فإن [Number.MAX\_SAFE\_INTEGER](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global%5FObjects/Number/MAX%5FSAFE%5FINTEGER?ref=arabroot.io) في JavaScript هو 9007199254740991، أي 253 \- 1، و`2**53 + 1 === 2**53` صحيح كما طبعته التجربة. وقد ذكرنا هذا الحد في مقال الأنظمة العددية، والآن سوف نرى أين يؤذيك فعلاً، وهو JSON.

فصيغة JSON نفسها لا تحدد حجماً للأرقام، ولكن [القسم 6 من RFC 8259](https://www.rfc-editor.org/rfc/rfc8259?ref=arabroot.io#section-6) ينبه إلى أن أغلب البرامج تقرأ الأرقام كـ `double`، وأن الأرقام الصحيحة مضمونة التوافق بين الأنظمة فقط إذا كانت قيمتها المطلقة لا تتجاوز 253 \- 1\. ولنفرض أن السيرفر يرسل طلباً معرفه 1234567890123456789، وهو معرف 64 بت عادي، فهذا ما حدث في التجربة:

- في Node.js أعطى `JSON.parse` القيمة 1234567890123456800، أي أن آخر ثلاث خانات تغيرت دون أي خطأ، فإذا استخدم الكلاينت هذا المعرف ليطلب تفاصيل الطلب فسوف يطلب طلباً آخر أو لا يجد شيئاً.
- في Python أعطى `json.loads` الرقم الصحيح، لأن `int` في Python بدقة غير محدودة، ولكن عندما حولناه إلى `float` أصبح 1234567890123456768، وهو نفس الرقم الذي خزنته JavaScript، ولكنها تطبع أقصر نص يعيده فتظهر 800 بدلاً من 768.
- في PostgreSQL أعطى `9007199254740993::float8::bigint` القيمة 9007199254740992، فإذا مر المعرف بعمود أو حساب من نوع `float8` في أي مرحلة فقد يتغير.

والحل أن ترسل المعرفات الكبيرة كنصوص في JSON، وهذا ما تفعله مواصفة [ProtoJSON](https://protobuf.dev/programming-guides/json/?ref=arabroot.io) في gRPC تلقائياً، فالأنواع `int64` و`uint64` تكتب في JSON كنص عشري، ولهذا السبب تحديداً، وقد شرحنا تصميم الواجهات وأنواع الحقول في [مقال تصميم واجهات API](https://arabroot.io/articles/%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D9%88%D8%A7%D8%AC%D9%87%D8%A7%D8%AA-api-rest-%D9%88-grpc-%D9%88-graphql/)، والمعرفات من نوع Snowflake في [مقال المعرفات الفريدة](https://arabroot.io/articles/%D8%A7%D9%84%D9%85%D8%B9%D8%B1%D9%81%D8%A7%D8%AA-%D8%A7%D9%84%D9%81%D8%B1%D9%8A%D8%AF%D8%A9-uuid-%D9%88-ulid-%D9%88-snowflake/#snowflake). وإذا كنت تستقبل JSON لا تتحكم فيه، فإن [JSON.parse](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global%5FObjects/JSON/parse?ref=arabroot.io) في النسخ الحديثة يمرر للدالة Reviver معاملاً ثالثاً فيه النص الأصلي للقيمة `context.source`، فتستطيع تحويله إلى `BigInt` قبل أن يضيع، وقد نجح ذلك في Node.js 22 فأعطى `1234567890123456789n`. ولكن انتبه إلى أن `JSON.stringify` لا يعرف `BigInt`، فقد رمى `TypeError: Do not know how to serialize a BigInt`، لذلك حوله إلى نص قبل الإرسال.

## الأنواع العشرية: ما الذي تحله وما الذي لا تحله

في [مقال الأنظمة العددية](https://arabroot.io/articles/%D8%A7%D9%84%D8%A7%D9%86%D8%B8%D9%85%D8%A9-%D8%A7%D9%84%D8%B9%D8%AF%D8%AF%D9%8A%D8%A9-%D9%88%D8%A7%D9%84%D8%AB%D9%86%D8%A7%D8%A6%D9%8A-%D9%84%D9%84%D9%85%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D9%86/#money) قلنا إن المال يحفظ بنوع عشري أو بعدد صحيح بأصغر وحدة، والآن سوف نرى ما الذي يحدث داخل هذه الأنواع، لأن فهمها يمنعك من خطأين: أن تظن أنها دقيقة في كل شيء، وأن تستخدمها في مكان لا تحتاجها فيه.

فالنوع [decimal](https://learn.microsoft.com/en-us/dotnet/api/system.decimal?ref=arabroot.io) في C# هو أيضاً فاصلة عائمة، ولكن بالأساس 10 بدلاً من 2، فهو يخزن عدداً صحيحاً بحجم 96 بت ومعه معامل قياس Scale يحدد مكان الفاصلة العشرية، وحجمه 16 بايتاً كما طبعت `sizeof(decimal)`، وأكبر قيمة له 79228162514264337593543950335\. وبما أن الأساس 10، فكل كسر عشري مثل 0.1 و0.01 يمثل بالضبط، فأعطى `0.1m + 0.2m` القيمة 0.3 والمقارنة بـ 0.3m صحيحة. ولكن الكسور التي لا تنتهي بالعشري ما زالت مقربة، فـ `1m / 3m * 3m` أعطى 0.9999999999999999999999999999 وليس 1، لأن الثلث بالعشري لا ينتهي، تماماً كما لا تنتهي 0.1 بالثنائي. ونفس الشيء في Python، فالوحدة [decimal](https://docs.python.org/3/library/decimal.html?ref=arabroot.io) تقرب كل عملية إلى دقة السياق Context، وهي 28 خانة افتراضياً، وفي Java يرمي [BigDecimal.divide](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/math/BigDecimal.html?ref=arabroot.io) الاستثناء `ArithmeticException` إذا كان الناتج كسراً لا ينتهي ولم تحدد له الدقة وطريقة التقريب، وهذا السلوك من Java أوضح في رأينا لأنه يجبرك على القرار.

وفي قواعد البيانات الصورة مشابهة، فـ PostgreSQL فيه `real` بحجم 4 بايتات و`double precision` أو `float8` بحجم 8 بايتات، وكلاهما IEEE 754، و`numeric` بحجم متغير يحفظ الخانات العشرية بالضبط، والجدول التالي يجمع ما ظهر في التجربة:

| الاختبار                      | real              | float8              | numeric                                    |
| ----------------------------- | ----------------- | ------------------- | ------------------------------------------ |
| 0.1 + 0.2                     | 0.3 (العرض مختصر) | 0.30000000000000004 | 0.3                                        |
| جمع 0.1 عشرة ملايين مرة       | 1.087937e+06      | 999999.9998389754   | 1000000.00                                 |
| round(2.5)                    |                   | 2                   | 3                                          |
| الحجم بالأمر pg\_column\_size | 4                 | 8                   | 12 للقيمة 123456.78                        |
| NaN وInfinity                 | نعم               | نعم                 | نعم، و Infinity فقط في عمود بدون دقة محددة |

والقاعدة في اختيار النوع، سواء في الكود أو في [قاعدة البيانات](https://arabroot.io/articles/%D8%A3%D8%B3%D8%A7%D8%B3%D9%8A%D8%A7%D8%AA-sql-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86/)، كما يلي:

- **المال والكميات التي لها خانات عشرية محددة قانوناً**، مثل الأسعار والضرائب والأرصدة: `decimal` أو `BigDecimal` أو `Decimal` أو `numeric(p, s)`، أو عدد صحيح بأصغر وحدة، والسبب أن المحاسب يقارن بالخانة الأخيرة، و[وثائق PostgreSQL](https://www.postgresql.org/docs/current/datatype-numeric.html?ref=arabroot.io) تنصح بـ `numeric` صراحة لهذه الحالة.
- **القياسات والحسابات العلمية والإحصاءات والرسوميات**: `double` أو `float8`، لأن القياس نفسه تقريبي، والسرعة والمساحة أهم من آخر خانة، والنوع العشري هنا أبطأ بكثير دون فائدة، فقد كان `decimal` في تجربتنا أبطأ من `double` قرابة 9 مرات في نفس التشغيل.
- **الأعداد الهائلة من القيم التقريبية**، مثل أوزان نماذج الذكاء الاصطناعي ومصفوفات الصور: `float` أو أصغر منه، وهذا موضوع القسم التالي.

## الأرقام العشرية في الذكاء الاصطناعي: fp16 وbfloat16 وfp8

نموذج اللغة عبارة عن مليارات الأرقام العشرية، وكل بت في كل رقم يكلف ذاكرة وعرض نطاق للذاكرة Memory Bandwidth، لذلك ظهرت أنواع أصغر من `float`، وكلها تتبع نفس فكرة IEEE 754، أي إشارة وأس وكسر، والفرق في عدد البتات لكل جزء، وهذا الاختيار هو الذي يحدد ما تخسره:

| النوع        | إشارة / أس / كسر | أكبر قيمة             | الرقم 0.1 المخزن (من التجربة) |
| ------------ | ---------------- | --------------------- | ----------------------------- |
| fp32 (float) | 1 / 8 / 23       | تقريباً 3.4 × 1038    | 0.100000001490116             |
| fp16 (Half)  | 1 / 5 / 10       | 65504                 | 0.0999755859375               |
| bfloat16     | 1 / 8 / 7        | نفس مجال fp32 تقريباً | 0.10009765625                 |
| fp8 E4M3     | 1 / 4 / 3        | 448                   | لم نقسه                       |
| fp8 E5M2     | 1 / 5 / 2        | 57344                 | لم نقسه                       |

وفي الجدول أعلاه لاحظ التالي:

- fp16 فيه كسر أطول من bfloat16، أي دقة أعلى، ولكن الأس 5 بتات فقط، فأكبر قيمة 65504، وفي التجربة أصبح الرقم 70000 لا نهاية في C# بالنوع [Half](https://learn.microsoft.com/en-us/dotnet/api/system.half?ref=arabroot.io)، ورمت Python `OverflowError`، والرقم 1e-8 أصبح صفراً، والرقم 2049 أصبح 2048\. ولاحظ أن C# طبعت `Half.MaxValue` كـ 65500 لأنها تطبع أقصر نص يعيد نفس القيمة، بينما القيمة الحقيقية 65504 كما طبعتها Python.
- bfloat16 هو ببساطة أعلى 16 بتاً من fp32، فبتات 0.1 فيه `0x3DCD` وهي بداية بتات `float` نفسها `0x3DCCCCCD` بعد التقريب، ولذلك له نفس الأس ونفس المجال كما تشرح [مدونة Google Cloud](https://cloud.google.com/blog/products/ai-machine-learning/bfloat16-the-secret-to-high-performance-on-cloud-tpus?ref=arabroot.io)، فالنوع ظهرت فكرته في فريق Google Brain واستخدم في معالجات TPU، فالرقم 70000 أصبح 70144 بدلاً من أن يفيض، و1e-8 بقي تقريباً كما هو، ولكن الدقة أقل بكثير، فالرقم 1000.5 أصبح 1000.
- fp8 له نوعان حسب ورقة [FP8 Formats for Deep Learning](https://arxiv.org/abs/2209.05433?ref=arabroot.io) من NVIDIA وArm وIntel: E4M3 بدقة أعلى ومجال حتى 448، وE5M2 بمجال حتى 57344 ودقة أقل، والقيم القصوى في الجدول من جدول الورقة نفسها.

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

![خمسة أشرطة بتات بنفس الألوان: fp32 بثمانية بتات أس و23 كسر، وfp16 بخمسة بتات أس و10 كسر، وbfloat16 بثمانية بتات أس و7 كسر، وfp8 E4M3 وE5M2، وبجانب كل شريط أكبر قيمة وقيمة 0.1 المخزنة، ثم جدول ذاكرة نموذج من 8 مليارات معامل: 32 GB بـ fp32 و16 GB بـ 16 بت و8 GB بـ 8 بت و4 GB بـ 4 بت](https://arabroot.io/content/images/2026/10/floating-point-05-formats.webp)

بتات الأس تحدد المجال وبتات الكسر تحدد الدقة، وكل نصف في الحجم ينصف ذاكرة النموذج

أما التكميم Quantization إلى int8 أو int4 فهو فكرة مختلفة، فالأوزان تحفظ كأعداد صحيحة صغيرة ومعها معامل قياس Scale مشترك لكل مجموعة من الأوزان، فتضرب العدد الصحيح في المعامل لتحصل على قيمة تقريبية للوزن الأصلي. والحساب البسيط للذاكرة هو عدد المعاملات في عدد البايتات لكل معامل: 4 بايتات في fp32 و2 في fp16 وbfloat16 و1 في fp8 وint8 ونصف بايت في 4 بت، فنموذج من 8 مليارات معامل يحتاج للأوزان وحدها قرابة 32 GB أو 16 أو 8 أو 4 على الترتيب. وقد شرحنا هذا الحساب بالتفصيل في [مقال كيف تعمل نماذج اللغة](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%B9%D9%85%D9%84-%D9%86%D9%85%D8%A7%D8%B0%D8%AC-%D8%A7%D9%84%D9%84%D8%BA%D8%A9-%D8%A7%D9%84%D9%83%D8%A8%D9%8A%D8%B1%D8%A9-llm-%D8%AE%D8%B7%D9%88%D8%A9-%D8%A8%D8%AE%D8%B7%D9%88%D8%A9/#memory)، وكيف تختار نوع التكميم لجهازك في [مقال تشغيل النماذج على جهازك](https://arabroot.io/articles/%D8%AA%D8%B4%D8%BA%D9%8A%D9%84-%D9%86%D9%85%D8%A7%D8%B0%D8%AC-%D8%A7%D9%84%D9%84%D8%BA%D8%A9-%D8%B9%D9%84%D9%89-%D8%AC%D9%87%D8%A7%D8%B2%D9%83-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#quantization).

## حادثتان موثقتان: Patriot وAriane 5

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

📡

في 25 فبراير 1991، أثناء عملية عاصفة الصحراء، لم تتمكن بطارية صواريخ Patriot في الظهران من تتبع صاروخ Scud واعتراضه، فسقط الصاروخ على ثكنة للجيش الأمريكي وقتل 28 جندياً. وبحسب [تقرير مكتب المحاسبة الحكومي الأمريكي GAO رقم IMTEC-92-26](https://www.gao.gov/products/imtec-92-26?ref=arabroot.io)، فإن ساعة النظام تعد الوقت بأعشار الثانية كعدد صحيح، ولكي يتوقع النظام مكان الصاروخ القادم يحول هذا العدد إلى رقم حقيقي، وبما أن مسجلات الحاسب 24 بتاً فقط، فإن التحويل فقد جزءاً من الدقة، وهذا الخطأ يكبر كلما طال تشغيل النظام. وكانت البطارية تعمل دون توقف أكثر من 100 ساعة، فانحرفت الساعة 0.3433 ثانية، وانزاحت منطقة البحث Range Gate قرابة 687 متراً عن مكان الصاروخ، فلم يره النظام. وكان الجيش قد تلقى بيانات من إسرائيل عن هذا الانحراف، وجهز نسخة معدلة من البرنامج، ولكنها وصلت إلى الظهران في 26 فبراير، أي بعد الحادثة بيوم.

والتقرير لا يذكر طريقة التمثيل الداخلية بالتفصيل، ولكننا نستطيع أن نعيد حساب أرقامه، فإذا قطعنا الرقم 0.1 بعد 23 بتاً من الفاصلة الثنائية دون تقريب، فالقيمة المخزنة 0.09999990463256836، والخطأ في كل عشر ثانية 9.5367e-08 ثانية، وبضربه في عدد الأعشار في كل فترة نحصل على هذا الجدول من تجربة Python، وبجانبه أرقام الملحق الثاني من تقرير GAO:

| مدة التشغيل | عدد الأعشار | الانحراف من حسابنا | الانحراف في تقرير GAO | انزياح منطقة البحث في التقرير |
| ----------- | ----------- | ------------------ | --------------------- | ----------------------------- |
| 1 ساعة      | 36,000      | 0.0034 ثانية       | 0.0034 ثانية          | 7 أمتار                       |
| 8 ساعات     | 288,000     | 0.0275 ثانية       | 0.0275 ثانية          | 55 متراً                      |
| 20 ساعة     | 720,000     | 0.0687 ثانية       | 0.0687 ثانية          | 137 متراً                     |
| 48 ساعة     | 1,728,000   | 0.1648 ثانية       | 0.1648 ثانية          | 330 متراً                     |
| 72 ساعة     | 2,592,000   | 0.2472 ثانية       | 0.2472 ثانية          | 494 متراً                     |
| 100 ساعة    | 3,600,000   | 0.3433 ثانية       | 0.3433 ثانية          | 687 متراً                     |

![ساعة تعد بأعشار الثانية تحول إلى رقم في مسجل 24 بت يخسر 0.000000095 ثانية في كل عشر، وتحتها رسم بياني لانزياح منطقة البحث بالمتر مقابل ساعات التشغيل من تقرير GAO: 7 أمتار بعد ساعة و137 بعد 20 ساعة و687 بعد 100 ساعة، مع خط عند 20 ساعة حيث يخرج الهدف من منطقة البحث](https://arabroot.io/content/images/2026/10/floating-point-06-patriot.webp)

الخطأ في كل عشر ثانية صغير جداً، ولكنه يضرب في عدد الأعشار فيكبر مع كل ساعة تشغيل

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

🚀

في 4 يونيو 1996 انفجر صاروخ Ariane 5 في رحلته الأولى بعد قرابة 40 ثانية من بدء الإطلاق، على ارتفاع 3700 متر تقريباً. وبحسب [تقرير لجنة التحقيق](https://esamultimedia.esa.int/docs/esa-x-1819eng.pdf?ref=arabroot.io) الذي [نشرته وكالة الفضاء الأوروبية ESA](https://www.esa.int/Newsroom/Press%5FReleases/Ariane%5F501%5F-%5FPresentation%5Fof%5FInquiry%5FBoard%5Freport?ref=arabroot.io)، فإن برنامج نظام الملاحة بالقصور الذاتي حول رقماً عشرياً من 64 بت إلى عدد صحيح بإشارة من 16 بت، وكان الرقم، وهو متغير يسمى BH أي الانحراف الأفقي Horizontal Bias، أكبر مما يتسع له 16 بت، فحدث خطأ Operand Error لأن هذا التحويل في كود Ada لم يكن محمياً، مع أن تحويلات أخرى بجانبه كانت محمية. والبرنامج منقول من Ariane 4، ومسار Ariane 5 فيه سرعة أفقية أعلى بكثير، ولم تستخدم بيانات مسار Ariane 5 في تحليل هذه المتغيرات. ثم أوقف نظام معالجة الأخطاء المعالج كما تنص المواصفة، والنظام الاحتياطي يشغل نفس البرنامج فتوقف قبله بدورة واحدة، والدالة التي سببت الخطأ لم يكن لها أي فائدة بعد الإقلاع أصلاً.

لاحظ أن Ariane 5 لم يفشل بسبب دقة الرقم العشري، فالرقم المحسوب كان صحيحاً، وإنما بسبب تحويله إلى نوع أصغر دون التحقق من المجال. ونفس الخطأ تستطيع أن تكتبه في C# اليوم، ففي التجربة حولنا 40000.5 إلى `short`، وأكبر قيمة لـ `short` هي 32767:

- بدون فحص Unchecked أعطى التحويل `-25536` بصمت، وهذا أسوأ من خطأ واضح، لأن البرنامج يكمل بقيمة خاطئة.
- مع [checked](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/statements/checked-and-unchecked?ref=arabroot.io) رمى `OverflowException`، وهذا ما حدث في Ariane، والفرق أن عليك أن تقرر ماذا تفعل بالاستثناء، لا أن توقف النظام كله.
- وتحويل `1e10` إلى `int` أعطى 2147483647، وتحويل NaN أعطى صفراً، لأن .NET 9 جعل هذه التحويلات تتوقف عند الحد الأقصى Saturating على x86 وx64 كما في [صفحة التغييرات](https://learn.microsoft.com/en-us/dotnet/core/compatibility/jit/9.0/fp-to-integer?ref=arabroot.io)، أي أن نفس الكود كان يعطي نتيجة مختلفة في النسخ السابقة.

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

## التجربة العملية: الكود والمخرجات كاملة

شغلنا كل الأمثلة في Containers منفصلة على جهاز x86-64 بإصدارات محددة من ال Images، حتى تستطيع إعادة التجربة بنفس النتائج. ضع الملفات الأربعة في مجلد واحد، ثم شغل كل لغة بالأمر الخاص بها من داخل المجلد.

### Python: البتات والمسافات والقيم الخاصة والجمع وساعة Patriot

الملف `floats.py` يستخدم الوحدة [struct](https://docs.python.org/3/library/struct.html?ref=arabroot.io) لقراءة البتات، والصيغة `e` فيها هي fp16، أما bfloat16 فحسبناه بأخذ أعلى 16 بتاً من `float` مع التقريب إلى الزوجي:

```python
import json, math, struct, sys, time
from decimal import Decimal, ROUND_HALF_UP, ROUND_HALF_EVEN
from fractions import Fraction

def bits64(x):
    (u,) = struct.unpack('>Q', struct.pack('>d', x))
    b = f'{u:064b}'
    return u, b[0], b[1:12], b[12:]

def bits32(x):
    (u,) = struct.unpack('>I', struct.pack('>f', x))
    b = f'{u:032b}'
    return u, b[0], b[1:9], b[9:]

print('== 1) the real bits')
for x in (0.1, 3.14):
    u, s, e, m = bits64(x)
    print(f'double {x}: 0x{u:016X}')
    print(f'  sign={s} exponent={e} ({int(e, 2)} - 1023 = {int(e, 2) - 1023})')
    print(f'  mantissa={m}')
    print(f'  value = (-1)^{s} x 1.{m[:12]}... x 2^{int(e, 2) - 1023}')
    print(f'  exact stored value = {Decimal(x)}')
    u, s, e, m = bits32(x)
    f32 = struct.unpack('>f', struct.pack('>f', x))[0]
    print(f'float  {x}: 0x{u:08X} sign={s} exponent={e} ({int(e, 2)} - 127 = {int(e, 2) - 127}) mantissa={m}')
    print(f'  exact stored value = {Decimal(f32)}')

print('== 2) precision in decimal digits')
print('float32(0.1) printed with 17 digits:', f'{struct.unpack(">f", struct.pack(">f", 0.1))[0]:.17g}')
print('sys.float_info.dig =', sys.float_info.dig, ' mant_dig =', sys.float_info.mant_dig)
print('16777217 as float32 ->', struct.unpack('>f', struct.pack('>f', 16777217.0))[0])
print('9007199254740993 as double ->', float(9007199254740993))

print('== 3) the gap between neighbours (ULP)')
for x in (1.0, 1000.0, 1e6, 1e9, 2.0**53, 1e16, 1e300):
    print(f'math.ulp({x:g}) = {math.ulp(x):.6g}')
print('math.nextafter(1.0, 2.0) =', repr(math.nextafter(1.0, 2.0)))
print('1e16 + 1 == 1e16 ?', 1e16 + 1 == 1e16)
print('sys.float_info.epsilon =', sys.float_info.epsilon)
now = 1791460000.0  # a Unix time in October 2026, in seconds
f32 = struct.unpack('>f', struct.pack('>f', now))[0]
up = struct.unpack('>f', struct.pack('>I', struct.unpack('>I', struct.pack('>f', now))[0] + 1))[0]
print(f'Unix time {now:.0f}: double gap = {math.ulp(now):.3g} s, float32 stores {f32:.0f}, float32 gap = {up - f32:.0f} s')

print('== 4) special values')
print('0.0 == -0.0 ?', 0.0 == -0.0, ' copysign(1, -0.0) =', math.copysign(1, -0.0))
inf, nan = float('inf'), float('nan')
print('inf - inf =', inf - inf, ' inf * 0 =', inf * 0, ' 1e308 * 10 =', 1e308 * 10)
print('nan == nan ?', nan == nan, ' math.isnan(nan) =', math.isnan(nan))
print('nan in [nan] ?', nan in [nan], ' float("nan") in [float("nan")] ?', float('nan') in [float('nan')])
print('max(nan, 1) =', max(nan, 1), ' max(1, nan) =', max(1, nan))
print('sorted([3, nan, 1, 2]) =', sorted([3, nan, 1, 2]))
print('smallest normal  =', sys.float_info.min)
print('smallest subnormal =', math.ulp(0.0), ' half of it =', math.ulp(0.0) / 2)
print('json.dumps(nan) =', json.dumps(nan))
try:
    json.dumps(nan, allow_nan=False)
except ValueError as e:
    print('json.dumps(nan, allow_nan=False) ->', e)
try:
    1.0 / 0.0
except ZeroDivisionError as e:
    print('1.0 / 0.0 ->', type(e).__name__, e)

print('== 5) rounding')
print('round(0.5), round(1.5), round(2.5), round(3.5) =', round(0.5), round(1.5), round(2.5), round(3.5))
print('round(2.675, 2) =', round(2.675, 2), ' because 2.675 is', Decimal(2.675))
print('Decimal("2.5") HALF_EVEN ->', Decimal('2.5').quantize(Decimal('1'), rounding=ROUND_HALF_EVEN),
      ' HALF_UP ->', Decimal('2.5').quantize(Decimal('1'), rounding=ROUND_HALF_UP))
print('Decimal("2.675") to 2 places HALF_UP ->', Decimal('2.675').quantize(Decimal('0.01'), rounding=ROUND_HALF_UP))

print('== 6) comparing')
a = 0.1 + 0.2
print('a == 0.3 ?', a == 0.3, ' abs(a - 0.3) =', abs(a - 0.3))
print('math.isclose(a, 0.3) =', math.isclose(a, 0.3))
print('math.isclose(1e-10, 0.0) =', math.isclose(1e-10, 0.0), ' with abs_tol=1e-9 ->', math.isclose(1e-10, 0.0, abs_tol=1e-9))
big1, big2 = 1e15 + 0.3, 1e15 + 0.5
print('abs(big1 - big2) < 1e-9 ?', abs(big1 - big2) < 1e-9, ' isclose ->', math.isclose(big1, big2))

print('== 7) summing 0.1 ten million times')
N = 10_000_000
t = time.perf_counter(); s = 0.0
for _ in range(N):
    s += 0.1
t1 = time.perf_counter() - t
t = time.perf_counter(); s2 = 0.0; c = 0.0
for _ in range(N):
    y = 0.1 - c
    tt = s2 + y
    c = (tt - s2) - y
    s2 = tt
t2 = time.perf_counter() - t
t = time.perf_counter(); s3 = math.fsum(0.1 for _ in range(N)); t3 = time.perf_counter() - t
t = time.perf_counter(); s4 = sum(Decimal('0.1') for _ in range(N)); t4 = time.perf_counter() - t
print(f'naive  {s!r:<22} error {s - 1e6:+.3e}  {t1:.2f}s')
print(f'kahan  {s2!r:<22} error {s2 - 1e6:+.3e}  {t2:.2f}s')
print(f'fsum   {s3!r:<22} error {s3 - 1e6:+.3e}  {t3:.2f}s')
print(f'Decimal {s4!s:<21}                  {t4:.2f}s')

print('== 8) the Patriot clock: 0.1 chopped to 23 bits after the binary point')
tenth = Fraction(int(Fraction(1, 10) * 2**23), 2**23)
err = Fraction(1, 10) - tenth
print('stored 0.1 =', float(tenth), ' error per tick =', f'{float(err):.4e}')
for hours in (1, 8, 20, 48, 72, 100):
    ticks = hours * 3600 * 10
    print(f'{hours:>3} h: {ticks:>9} ticks, clock drift = {float(err * ticks):.4f} s')

print('== 9) big integers in JSON')
doc = '{"id": 1234567890123456789}'
print('json.loads ->', json.loads(doc)['id'])
print('as float  ->', float(json.loads(doc)['id']), '->', int(float(json.loads(doc)['id'])))

print('== 10) fp16 and bfloat16')
def f16(x):
    return struct.unpack('<e', struct.pack('<e', x))[0]
def bf16_bits(x):
    (u,) = struct.unpack('>I', struct.pack('>f', x))
    r = ((u >> 16) & 1) + 0x7FFF
    return (u + r) >> 16
def bf16(x):
    return struct.unpack('>f', struct.pack('>I', bf16_bits(x) << 16))[0]
for x in (0.1, 3.14159265, 1000.5, 65504.0, 70000.0, 1e-8):
    try:
        h = f16(x)
    except OverflowError:
        h = 'OverflowError'
    print(f'{x:>12g} -> fp16 {h!s:<22} bf16 {bf16(x)!r}')
print('fp16 bits of 0.1 = 0x%04X' % struct.unpack('<H', struct.pack('<e', 0.1))[0], ' bf16 bits of 0.1 = 0x%04X' % bf16_bits(0.1))
```

```bash
docker run --rm --name lab-float-python -v "$PWD:/lab" -w /lab python:3.13.8-slim python floats.py
```

والمخرج سوف يكون كما يلي:

```text
== 1) the real bits
double 0.1: 0x3FB999999999999A
  sign=0 exponent=01111111011 (1019 - 1023 = -4)
  mantissa=1001100110011001100110011001100110011001100110011010
  value = (-1)^0 x 1.100110011001... x 2^-4
  exact stored value = 0.1000000000000000055511151231257827021181583404541015625
float  0.1: 0x3DCCCCCD sign=0 exponent=01111011 (123 - 127 = -4) mantissa=10011001100110011001101
  exact stored value = 0.100000001490116119384765625
double 3.14: 0x40091EB851EB851F
  sign=0 exponent=10000000000 (1024 - 1023 = 1)
  mantissa=1001000111101011100001010001111010111000010100011111
  value = (-1)^0 x 1.100100011110... x 2^1
  exact stored value = 3.140000000000000124344978758017532527446746826171875
float  3.14: 0x4048F5C3 sign=0 exponent=10000000 (128 - 127 = 1) mantissa=10010001111010111000011
  exact stored value = 3.1400001049041748046875
== 2) precision in decimal digits
float32(0.1) printed with 17 digits: 0.10000000149011612
sys.float_info.dig = 15  mant_dig = 53
16777217 as float32 -> 16777216.0
9007199254740993 as double -> 9007199254740992.0
== 3) the gap between neighbours (ULP)
math.ulp(1) = 2.22045e-16
math.ulp(1000) = 1.13687e-13
math.ulp(1e+06) = 1.16415e-10
math.ulp(1e+09) = 1.19209e-07
math.ulp(9.0072e+15) = 2
math.ulp(1e+16) = 2
math.ulp(1e+300) = 1.48702e+284
math.nextafter(1.0, 2.0) = 1.0000000000000002
1e16 + 1 == 1e16 ? True
sys.float_info.epsilon = 2.220446049250313e-16
Unix time 1791460000: double gap = 2.38e-07 s, float32 stores 1791459968, float32 gap = 128 s
== 4) special values
0.0 == -0.0 ? True  copysign(1, -0.0) = -1.0
inf - inf = nan  inf * 0 = nan  1e308 * 10 = inf
nan == nan ? False  math.isnan(nan) = True
nan in [nan] ? True  float("nan") in [float("nan")] ? False
max(nan, 1) = nan  max(1, nan) = 1
sorted([3, nan, 1, 2]) = [3, nan, 1, 2]
smallest normal  = 2.2250738585072014e-308
smallest subnormal = 5e-324  half of it = 0.0
json.dumps(nan) = NaN
json.dumps(nan, allow_nan=False) -> Out of range float values are not JSON compliant: nan
1.0 / 0.0 -> ZeroDivisionError float division by zero
== 5) rounding
round(0.5), round(1.5), round(2.5), round(3.5) = 0 2 2 4
round(2.675, 2) = 2.67  because 2.675 is 2.67499999999999982236431605997495353221893310546875
Decimal("2.5") HALF_EVEN -> 2  HALF_UP -> 3
Decimal("2.675") to 2 places HALF_UP -> 2.68
== 6) comparing
a == 0.3 ? False  abs(a - 0.3) = 5.551115123125783e-17
math.isclose(a, 0.3) = True
math.isclose(1e-10, 0.0) = False  with abs_tol=1e-9 -> True
abs(big1 - big2) < 1e-9 ? False  isclose -> True
== 7) summing 0.1 ten million times
naive  999999.9998389754      error -1.610e-04  3.09s
kahan  1000000.0              error +0.000e+00  6.37s
fsum   1000000.0              error +0.000e+00  0.78s
Decimal 1000000.0                              7.35s
== 8) the Patriot clock: 0.1 chopped to 23 bits after the binary point
stored 0.1 = 0.09999990463256836  error per tick = 9.5367e-08
  1 h:     36000 ticks, clock drift = 0.0034 s
  8 h:    288000 ticks, clock drift = 0.0275 s
 20 h:    720000 ticks, clock drift = 0.0687 s
 48 h:   1728000 ticks, clock drift = 0.1648 s
 72 h:   2592000 ticks, clock drift = 0.2472 s
100 h:   3600000 ticks, clock drift = 0.3433 s
== 9) big integers in JSON
json.loads -> 1234567890123456789
as float  -> 1.2345678901234568e+18 -> 1234567890123456768
== 10) fp16 and bfloat16
         0.1 -> fp16 0.0999755859375        bf16 0.10009765625
     3.14159 -> fp16 3.140625               bf16 3.140625
      1000.5 -> fp16 1000.5                 bf16 1000.0
       65504 -> fp16 65504.0                bf16 65536.0
       70000 -> fp16 OverflowError          bf16 70144.0
       1e-08 -> fp16 0.0                    bf16 1.0011717677116394e-08
fp16 bits of 0.1 = 0x2E66  bf16 bits of 0.1 = 0x3DCD
```

في المخرج أعلاه لاحظ التالي:

- القسم الأول يطبع البتات مقسمة إلى إشارة وأس وكسر، وهي نفس البتات التي في الصورة الأولى، و`Decimal(x)` يطبع القيمة المخزنة بكل خاناتها دون تقريب.
- `sorted([3, nan, 1, 2])` أعاد القائمة دون ترتيب، لأن كل مقارنة مع NaN خطأ، فتظن الخوارزمية أن القائمة مرتبة.
- القسم الثامن يعيد حساب جدول تقرير Patriot بالكسور الدقيقة `Fraction`، حتى لا يدخل خطأ `double` نفسه في الحساب.

### Node.js: البتات والمعرفات الكبيرة وتقريب JavaScript

الملف `floats.js` يقرأ البتات بـ `DataView`، والدالة `nextUp` تزيد بتات الرقم بواحد لتحصل على الرقم التالي، وهذا يعمل للأرقام الموجبة بفضل الإزاحة التي شرحناها في أول المقال:

```javascript
const dv = new DataView(new ArrayBuffer(8));
const bits = (x) => { dv.setFloat64(0, x); return dv.getBigUint64(0); };
const fromBits = (u) => { dv.setBigUint64(0, u); return dv.getFloat64(0); };
const nextUp = (x) => fromBits(bits(x) + 1n);

console.log('== 1) the real bits');
for (const x of [0.1, 3.14]) console.log(x, '0x' + bits(x).toString(16).toUpperCase(), ' Math.fround:', Math.fround(x));

console.log('== 2) the gap between neighbours');
console.log('Number.EPSILON =', Number.EPSILON);
for (const x of [1, 1e6, 2 ** 53, 1e16]) console.log(`next after ${x} = ${nextUp(x)}  gap = ${nextUp(x) - x}`);

console.log('== 3) big integers');
console.log('MAX_SAFE_INTEGER =', Number.MAX_SAFE_INTEGER, ' isSafeInteger(2**53) =', Number.isSafeInteger(2 ** 53));
console.log('2**53 + 1 =', 2 ** 53 + 1, ' 2**53 + 1 === 2**53 ?', 2 ** 53 + 1 === 2 ** 53);
const doc = '{"id": 1234567890123456789, "id_str": "1234567890123456789"}';
const o = JSON.parse(doc);
console.log('JSON.parse id     ->', o.id);
console.log('JSON.parse id_str ->', o.id_str, '-> BigInt', BigInt(o.id_str));
const keep = JSON.parse(doc, (k, v, ctx) => (k === 'id' && ctx && ctx.source) ? BigInt(ctx.source) : v);
console.log('reviver with source ->', keep.id, typeof keep.id);
try { JSON.stringify({ id: 10n }); } catch (e) { console.log('JSON.stringify(10n) ->', e.constructor.name + ':', e.message); }

console.log('== 4) special values');
console.log('0 === -0 ?', 0 === -0, ' Object.is(0, -0) =', Object.is(0, -0), ' 1 / -0 =', 1 / -0);
console.log('NaN === NaN ?', NaN === NaN, ' Object.is(NaN, NaN) =', Object.is(NaN, NaN));
console.log('[NaN].indexOf(NaN) =', [NaN].indexOf(NaN), ' [NaN].includes(NaN) =', [NaN].includes(NaN));
console.log('Math.max(1, NaN) =', Math.max(1, NaN), ' JSON.stringify([NaN, Infinity, -0]) =', JSON.stringify([NaN, Infinity, -0]));
console.log('Number.MIN_VALUE =', Number.MIN_VALUE, ' Number.MIN_VALUE / 2 =', Number.MIN_VALUE / 2);

console.log('== 5) rounding');
console.log('Math.round(2.5), Math.round(-2.5), Math.round(-0.5) =', Math.round(2.5), Math.round(-2.5), Math.round(-0.5));
console.log('(2.5).toFixed(0) =', (2.5).toFixed(0), ' (1.005).toFixed(2) =', (1.005).toFixed(2), ' (1.005).toPrecision(20) =', (1.005).toPrecision(20));

console.log('== 6) summing 0.1 ten million times');
const N = 10_000_000;
let t = performance.now(), s = 0;
for (let i = 0; i < N; i++) s += 0.1;
const t1 = performance.now() - t;
t = performance.now(); let s2 = 0, c = 0;
for (let i = 0; i < N; i++) { const y = 0.1 - c; const tt = s2 + y; c = (tt - s2) - y; s2 = tt; }
const t2 = performance.now() - t;
t = performance.now(); let cents = 0;
for (let i = 0; i < N; i++) cents += 10;
const t3 = performance.now() - t;
t = performance.now(); let f = Math.fround(0);
for (let i = 0; i < N; i++) f = Math.fround(f + Math.fround(0.1));
const t4 = performance.now() - t;
console.log(`naive double  ${s}  ${t1.toFixed(0)} ms`);
console.log(`kahan double  ${s2}  ${t2.toFixed(0)} ms`);
console.log(`integer cents ${cents / 100}  ${t3.toFixed(0)} ms`);
console.log(`naive float32 ${f}  ${t4.toFixed(0)} ms`);
```

```bash
docker run --rm --name lab-float-node -v "$PWD:/lab" -w /lab node:22.20.0-alpine node floats.js
```

والمخرج سوف يكون كما يلي:

```text
== 1) the real bits
0.1 0x3FB999999999999A  Math.fround: 0.10000000149011612
3.14 0x40091EB851EB851F  Math.fround: 3.140000104904175
== 2) the gap between neighbours
Number.EPSILON = 2.220446049250313e-16
next after 1 = 1.0000000000000002  gap = 2.220446049250313e-16
next after 1000000 = 1000000.0000000001  gap = 1.1641532182693481e-10
next after 9007199254740992 = 9007199254740994  gap = 2
next after 10000000000000000 = 10000000000000002  gap = 2
== 3) big integers
MAX_SAFE_INTEGER = 9007199254740991  isSafeInteger(2**53) = false
2**53 + 1 = 9007199254740992  2**53 + 1 === 2**53 ? true
JSON.parse id     -> 1234567890123456800
JSON.parse id_str -> 1234567890123456789 -> BigInt 1234567890123456789n
reviver with source -> 1234567890123456789n bigint
JSON.stringify(10n) -> TypeError: Do not know how to serialize a BigInt
== 4) special values
0 === -0 ? true  Object.is(0, -0) = false  1 / -0 = -Infinity
NaN === NaN ? false  Object.is(NaN, NaN) = true
[NaN].indexOf(NaN) = -1  [NaN].includes(NaN) = true
Math.max(1, NaN) = NaN  JSON.stringify([NaN, Infinity, -0]) = [null,null,0]
Number.MIN_VALUE = 5e-324  Number.MIN_VALUE / 2 = 0
== 5) rounding
Math.round(2.5), Math.round(-2.5), Math.round(-0.5) = 3 -2 -0
(2.5).toFixed(0) = 3  (1.005).toFixed(2) = 1.00  (1.005).toPrecision(20) = 1.0049999999999998934
== 6) summing 0.1 ten million times
naive double  999999.9998389754  41 ms
kahan double  1000000  59 ms
integer cents 1000000  19 ms
naive float32 1087937  39 ms
```

في المخرج أعلاه لاحظ التالي:

- `Math.fround` يقرب الرقم إلى أقرب [float](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global%5FObjects/Math/fround?ref=arabroot.io) ثم يعيده `Number`، ولهذا استخدمناه لمحاكاة جمع float32 فظهر نفس الرقم 1087937 الذي ظهر في C# وPostgreSQL.
- الحقل `id_str` وصل سليماً لأنه نص، والحقل `id` تغير، والـ Reviver مع `context.source` أنقذ القيمة الأصلية.
- `(1.005).toFixed(2)` أعطى `1.00` لأن القيمة المخزنة 1.0049999999999998934 كما طبعها `toPrecision(20)`.

### C#: الأنواع والتقريب والتحويل إلى عدد صحيح

الملف `floats.cs` يعمل دون مشروع بالأمر `dotnet run floats.cs`، وهي ميزة التطبيقات المكونة من ملف واحد في .NET 10:

```csharp
using System;
using System.Diagnostics;

Console.WriteLine("== 1) the real bits");
foreach (double x in new[] { 0.1, 3.14 })
{
    long b = BitConverter.DoubleToInt64Bits(x);
    int sign = (int)(b >> 63), exp = (int)((b >> 52) & 0x7FF);
    long frac = b & 0xFFFFFFFFFFFFF;
    Console.WriteLine($"{x}: 0x{b:X16} sign={sign} exponent={exp}-1023={exp - 1023} fraction=0x{frac:X13}");
    Console.WriteLine($"  float: 0x{BitConverter.SingleToInt32Bits((float)x):X8}  (float){x} = {(float)x:G9}");
}

Console.WriteLine("== 2) neighbours and the confusing double.Epsilon");
Console.WriteLine($"Math.BitIncrement(1.0) - 1.0 = {Math.BitIncrement(1.0) - 1.0:R}");
Console.WriteLine($"double.Epsilon = {double.Epsilon:R}  (the smallest subnormal, not the machine epsilon)");
Console.WriteLine($"float: 16777216f + 1f = {16777216f + 1f:R}");
float million = 1_000_000f;
Console.WriteLine($"float gap at 1 = {MathF.BitIncrement(1f) - 1f:R}  float gap at 1e6 = {MathF.BitIncrement(million) - million:R}  (1e6f + 0.1f) - 1e6f = {(million + 0.1f) - million:R}");

Console.WriteLine("== 3) special values");
double zero = 0.0, negZero = -0.0;
Console.WriteLine($"0.0 == -0.0: {zero == negZero}  1/-0.0 = {1 / negZero}  double.IsNegative(-0.0) = {double.IsNegative(negZero)}");
double nan = double.NaN;
Console.WriteLine($"NaN == NaN: {nan == double.NaN}  NaN.Equals(NaN): {nan.Equals(nan)}  double.IsNaN: {double.IsNaN(nan)}");
Console.WriteLine($"1.0 / 0.0 = {1.0 / zero}  0.0 / 0.0 = {zero / zero}  Math.Sqrt(-1) = {Math.Sqrt(-1)}");
var arr = new[] { 3.0, double.NaN, 1.0, 2.0 }; Array.Sort(arr);
Console.WriteLine($"Array.Sort with NaN: [{string.Join(", ", arr)}]");

Console.WriteLine("== 4) rounding");
Console.WriteLine($"Math.Round(0.5)={Math.Round(0.5)} Math.Round(1.5)={Math.Round(1.5)} Math.Round(2.5)={Math.Round(2.5)}");
Console.WriteLine($"Math.Round(2.5, MidpointRounding.AwayFromZero) = {Math.Round(2.5, MidpointRounding.AwayFromZero)}");
Console.WriteLine($"Math.Round(2.675, 2, AwayFromZero) = {Math.Round(2.675, 2, MidpointRounding.AwayFromZero)}");
Console.WriteLine($"Math.Round(2.675m, 2, AwayFromZero) = {Math.Round(2.675m, 2, MidpointRounding.AwayFromZero)}");
Console.WriteLine($"(2.5).ToString(\"F0\") = {(2.5).ToString("F0")}");

Console.WriteLine("== 5) float to integer: what happened to Ariane 5");
double bh = 40000.5;
Console.WriteLine($"(short){bh} unchecked = {unchecked((short)bh)}");
try { short s = checked((short)bh); Console.WriteLine(s); }
catch (OverflowException e) { Console.WriteLine($"checked((short){bh}) -> {e.GetType().Name}: {e.Message}"); }
double huge = 1e10;
Console.WriteLine($"(int)1e10 = {(int)huge}  (int)NaN = {(int)nan}");

Console.WriteLine("== 6) decimal");
Console.WriteLine($"0.1m + 0.2m = {0.1m + 0.2m}  equal to 0.3m: {0.1m + 0.2m == 0.3m}");
Console.WriteLine($"1m / 3m * 3m = {1m / 3m * 3m}");
Console.WriteLine($"decimal.MaxValue = {decimal.MaxValue}");
Console.WriteLine($"sizeof: float {sizeof(float)}, double {sizeof(double)}, decimal {sizeof(decimal)}, Half 2");

Console.WriteLine("== 7) Half (fp16)");
Console.WriteLine($"(Half)0.1 = {(Half)0.1}  bits 0x{BitConverter.HalfToUInt16Bits((Half)0.1):X4}  Half.MaxValue = {Half.MaxValue}  (Half)70000 = {(Half)70000.0}");
Console.WriteLine($"(Half)2049 = {(Half)2049.0}  Half.Epsilon = {Half.Epsilon}");

Console.WriteLine("== 8) summing 0.1 ten million times");
const int N = 10_000_000;
var sw = Stopwatch.StartNew(); double sum = 0; for (int i = 0; i < N; i++) sum += 0.1; var t1 = sw.ElapsedMilliseconds;
sw.Restart(); double ks = 0, c = 0; for (int i = 0; i < N; i++) { double y = 0.1 - c; double t = ks + y; c = (t - ks) - y; ks = t; } var t2 = sw.ElapsedMilliseconds;
sw.Restart(); decimal ds = 0; for (int i = 0; i < N; i++) ds += 0.1m; var t3 = sw.ElapsedMilliseconds;
sw.Restart(); float fs = 0; for (int i = 0; i < N; i++) fs += 0.1f; var t4 = sw.ElapsedMilliseconds;
Console.WriteLine($"double naive  {sum:R}  {t1} ms");
Console.WriteLine($"double kahan  {ks:R}  {t2} ms");
Console.WriteLine($"decimal       {ds}  {t3} ms");
Console.WriteLine($"float naive   {fs:R}  {t4} ms");
```

```bash
docker run --rm --name lab-float-dotnet -v "$PWD:/lab" -w /lab mcr.microsoft.com/dotnet/sdk:10.0.100 dotnet run floats.cs
```

والمخرج سوف يكون كما يلي:

```text
== 1) the real bits
0.1: 0x3FB999999999999A sign=0 exponent=1019-1023=-4 fraction=0x999999999999A
  float: 0x3DCCCCCD  (float)0.1 = 0.100000001
3.14: 0x40091EB851EB851F sign=0 exponent=1024-1023=1 fraction=0x91EB851EB851F
  float: 0x4048F5C3  (float)3.14 = 3.1400001
== 2) neighbours and the confusing double.Epsilon
Math.BitIncrement(1.0) - 1.0 = 2.220446049250313E-16
double.Epsilon = 5E-324  (the smallest subnormal, not the machine epsilon)
float: 16777216f + 1f = 16777216
float gap at 1 = 1.1920929E-07  float gap at 1e6 = 0.0625  (1e6f + 0.1f) - 1e6f = 0.125
== 3) special values
0.0 == -0.0: True  1/-0.0 = -Infinity  double.IsNegative(-0.0) = True
NaN == NaN: False  NaN.Equals(NaN): True  double.IsNaN: True
1.0 / 0.0 = Infinity  0.0 / 0.0 = NaN  Math.Sqrt(-1) = NaN
Array.Sort with NaN: [NaN, 1, 2, 3]
== 4) rounding
Math.Round(0.5)=0 Math.Round(1.5)=2 Math.Round(2.5)=2
Math.Round(2.5, MidpointRounding.AwayFromZero) = 3
Math.Round(2.675, 2, AwayFromZero) = 2.68
Math.Round(2.675m, 2, AwayFromZero) = 2.68
(2.5).ToString("F0") = 2
== 5) float to integer: what happened to Ariane 5
(short)40000.5 unchecked = -25536
checked((short)40000.5) -> OverflowException: Arithmetic operation resulted in an overflow.
(int)1e10 = 2147483647  (int)NaN = 0
== 6) decimal
0.1m + 0.2m = 0.3  equal to 0.3m: True
1m / 3m * 3m = 0.9999999999999999999999999999
decimal.MaxValue = 79228162514264337593543950335
sizeof: float 4, double 8, decimal 16, Half 2
== 7) Half (fp16)
(Half)0.1 = 0.1  bits 0x2E66  Half.MaxValue = 65500  (Half)70000 = Infinity
(Half)2049 = 2048  Half.Epsilon = 6E-08
== 8) summing 0.1 ten million times
double naive  999999.9998389754  22 ms
double kahan  1000000  81 ms
decimal       1000000.0  196 ms
float naive   1087937  22 ms
```

في المخرج أعلاه لاحظ التالي:

- `(Half)0.1` طبع 0.1 مع أن القيمة المخزنة 0.0999755859375 كما طبعتها Python، لأن C# تطبع أقصر نص يعيد نفس القيمة، وبتاته `0x2E66` مطابقة لبتات Python.
- `NaN.Equals(NaN)` صحيح بينما `==` خطأ، وهذا مذكور صراحة في [توثيق Double.Equals](https://learn.microsoft.com/en-us/dotnet/api/system.double.equals?ref=arabroot.io)، ونتيجته أن NaN يعمل كمفتاح في `Dictionary` بينما يفشل في المقارنة العادية.
- لاحظ أيضاً أن المترجم رفض في المحاولة الأولى السطر `(int)1e10` برسالة `error CS0221: Constant value '10000000000' cannot be converted to a 'int'`، لأن القيمة ثابتة ويعرف أنها لا تتسع، لذلك وضعناها في متغير حتى يحدث التحويل وقت التشغيل.

### PostgreSQL: real وfloat8 وnumeric جنباً إلى جنب

الملف `floats.sql` ينفذ نفس الاختبارات داخل قاعدة البيانات، ثم ينشئ جدولاً مؤقتاً فيه عشرة ملايين صف ويجمع العمود بالأنواع الثلاثة:

```sql
\pset footer off
\echo == 1) the same sum in three types
SELECT 0.1::float8 + 0.2::float8 AS float8_sum,
       0.1::real + 0.2::real     AS real_sum,
       0.1::numeric + 0.2::numeric AS numeric_sum,
       0.1::float8 + 0.2::float8 = 0.3::float8 AS float8_equal;
\echo == 2) what real really stores
SELECT 0.1::real AS shown, 0.1::real::float8 AS stored, 16777217::real::numeric AS big_real;
\echo == 3) rounding: numeric vs float8
SELECT round(2.5::numeric) AS numeric_2_5, round(2.5::float8) AS float8_2_5,
       round(3.5::float8) AS float8_3_5, round(2.675::numeric, 2) AS numeric_2_675;
\echo == 4) NaN and Infinity
SELECT 'NaN'::float8 = 'NaN'::float8 AS nan_equal,
       'NaN'::float8 > 'Infinity'::float8 AS nan_biggest,
       'Infinity'::numeric AS numeric_inf,
       '-0'::float8 AS neg_zero, '-0'::float8 = 0 AS neg_zero_equal;
\echo == 5) big integers through float8
SELECT 9007199254740993::bigint AS exact,
       9007199254740993::float8::bigint AS through_float8;
\echo == 6) summing 0.1 ten million times
CREATE TEMP TABLE t AS SELECT 0.1::float8 AS f, 0.1::real AS r, 0.1::numeric(12,2) AS n FROM generate_series(1, 10000000);
\timing on
SELECT sum(f) AS sum_float8 FROM t;
SELECT sum(r) AS sum_real FROM t;
SELECT sum(n) AS sum_numeric FROM t;
\timing off
SELECT pg_column_size(1.5::real) AS real_bytes, pg_column_size(1.5::float8) AS float8_bytes,
       pg_column_size(123456.78::numeric) AS numeric_bytes;
SELECT version();
```

```bash
docker run -d --name lab-float-pg -e POSTGRES_PASSWORD=lab -v "$PWD:/lab" postgres:17.11
docker exec lab-float-pg psql -U postgres -f /lab/floats.sql
docker rm -f -v lab-float-pg
```

والمخرج سوف يكون كما يلي:

```text
== 1) the same sum in three types
     float8_sum      | real_sum | numeric_sum | float8_equal
---------------------+----------+-------------+--------------
 0.30000000000000004 |      0.3 |         0.3 | f

== 2) what real really stores
 shown |       stored        | big_real
-------+---------------------+----------
   0.1 | 0.10000000149011612 | 16777200

== 3) rounding: numeric vs float8
 numeric_2_5 | float8_2_5 | float8_3_5 | numeric_2_675
-------------+------------+------------+---------------
           3 |          2 |          4 |          2.68

== 4) NaN and Infinity
 nan_equal | nan_biggest | numeric_inf | neg_zero | neg_zero_equal
-----------+-------------+-------------+----------+----------------
 t         | t           |    Infinity |       -0 | t

== 5) big integers through float8
      exact       |  through_float8
------------------+------------------
 9007199254740993 | 9007199254740992

== 6) summing 0.1 ten million times
SELECT 10000000
Timing is on.
    sum_float8
-------------------
 999999.9998389754

Time: 2159.382 ms (00:02.159)
   sum_real
--------------
 1.087937e+06

Time: 1731.492 ms (00:01.731)
 sum_numeric
-------------
  1000000.00

Time: 2143.081 ms (00:02.143)
Timing is off.
 real_bytes | float8_bytes | numeric_bytes
------------+--------------+---------------
          4 |            8 |            12

                                                       version
----------------------------------------------------------------------------------------------------------------------
 PostgreSQL 17.11 (Debian 17.11-1.pgdg13+2) on x86_64-pc-linux-gnu, compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit
```

في المخرج أعلاه لاحظ التالي:

- `real_sum` ظهر 0.3 لأن PostgreSQL يعرض أقصر نص يعيد القيمة، أما القيمة المخزنة فليست 0.3، كما ظهر في السطر التالي عند تحويل 0.1 إلى `float8`.
- `'-0'::float8` عرض `-0` وهو يساوي الصفر في المقارنة، تماماً كما في اللغات.
- الخيار `-v` في الأمر الأخير يحذف ال Volume الذي ينشئه Docker Image الخاص بـ PostgreSQL تلقائياً، فلا يبقى على جهازك بعد التجربة.

## أخطاء شائعة

1. **المقارنة بقيمة سماح ثابتة لكل الأحجام:** `1e-9` أكبر من اللازم للأرقام الصغيرة وأصغر من المسافة بين الأرقام الكبيرة، فاستخدم قيمة سماح نسبية مثل `math.isclose`.
2. **`double.Epsilon` في C# كقيمة سماح:** هو 5E-324 وليس إبسلون الآلة، فالمقارنة به تساوي ==.
3. **الجمع الطويل في `float`:** عداد أو مجموع يكبر مع الوقت يجب أن يكون `double` على الأقل، أو عدداً صحيحاً، وإلا توقف عن الزيادة أو زاد أكثر مما يجب كما رأينا مع 1087937.
4. **الوقت في رقم عشري صغير:** وقت Unix في `float` فجوته 128 ثانية، وعداد بأعشار الثانية مضروب في 0.1 يتراكم خطأه كما في Patriot، فاحفظ الوقت كعدد صحيح بأصغر وحدة.
5. **إدخال NaN إلى البيانات:** يكسر الترتيب في Python ويتصرف بشكل مختلف في كل لغة وفي قاعدة البيانات، فتحقق منه عند الإدخال وارفضه.
6. **تقريب `double` إلى خانات عشرية للعرض أو المال:** اللغات تختلف في النتيجة لنفس الرقم، فقرب بنوع عشري وحدد طريقة التقريب صراحة.
7. **معرفات 64 بت كأرقام في JSON:** أرسلها كنصوص، فـ JavaScript تغير آخر خاناتها بصمت.
8. **تحويل رقم عشري إلى عدد صحيح أصغر دون فحص:** هذا خطأ Ariane 5 نفسه، فافحص المجال أو استخدم `checked` وقرر ما يحدث عند الفشل.
9. **اختيار fp16 لقيم مجالها كبير:** أكبر قيمة فيه 65504، فإذا كانت قيمك قد تتجاوز ذلك أو تصغر جداً فاستخدم bfloat16 أو fp32.

## تدريب عملي

- فك بيدك بتات الرقم `-2.5` في `double`: اكتب الإشارة والأس المخزن والكسر، ثم تأكد من الناتج بالأمر `struct.pack('>d', -2.5).hex()` في Python.
- احسب المسافة بين الأرقام عند 224 في `float`، ثم فسر لماذا لا يستطيع عداد من النوع `float` أن يتجاوز 16777216 إذا زاد بواحد في كل مرة، وجربه في الكود.
- اكتب دالة `isClose` في C# أو JavaScript بنفس معادلة `math.isclose`، واختبرها على الأمثلة الثلاثة في قسم المقارنة.
- عدل جدول PostgreSQL في التجربة ليستخدم `numeric` بدون دقة محددة، ثم جرب إدخال `'Infinity'` فيه وفي عمود `numeric(12,2)`، وقارن الرسالتين.

## الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

- الرقم في IEEE 754 إشارة وأس مع إزاحة وكسر مع بت مخفي، فدقة `float` 24 بتاً، أي 6 إلى 9 خانات عشرية، ودقة `double` 53 بتاً، أي 15 إلى 17 خانة.
- المسافة بين الرقم والذي يليه تتضاعف كلما تضاعف الرقم، لذلك تضيع الإضافات الصغيرة إلى الأرقام الكبيرة، ويفقد `double` الأرقام الصحيحة الفردية بعد 253.
- الصفر السالب واللانهاية و NaN قيم حقيقية في المعيار، و NaN لا يساوي نفسه في الكود ولكنه يساوي نفسه في PostgreSQL، فامنعه من الدخول إلى بياناتك.
- التقريب الافتراضي إلى الزوجي يمنع الانحياز، ولكنه يفاجئ من ينتظر 3 من `round(2.5)`، فحدد طريقة التقريب صراحة بنوع عشري عندما يهمك الناتج.
- قارن بقيمة سماح نسبية، واجمع الأعداد الكثيرة بـ Kahan أو `math.fsum` أو بنوع عشري أو بعدد صحيح بأصغر وحدة.
- الأنواع العشرية دقيقة في الكسور العشرية فقط، والثلث يبقى مقرباً فيها، وأنواع الذكاء الاصطناعي الصغيرة تختار بين المجال والدقة.
- Patriot خطأ دقة تراكم مع وقت التشغيل، وAriane 5 تحويل من رقم عشري إلى عدد صحيح من 16 بت دون حماية، ولكل منهما درس مختلف.

وفي مرحلة [أساسيات علوم الحاسب](https://arabroot.io/start/#stage-foundations) نفسها تجد المقالات التي يعتمد عليها هذا المقال أو يكملها:

[![](https://arabroot.io/content/images/2026/10/number-systems-cover-cover.webp)دليلالأنظمة العددية للمبرمجين: من Binary إلى Floating Pointلماذا يظهر القرص 465 بدل 500، ويصبح العداد سالباً فجأة، ولا يساوي 0.1 + 0.2 القيمة 0.3؟ الإجابة في طريقة تخزين الأرقام.](https://arabroot.io/articles/%D8%A7%D9%84%D8%A7%D9%86%D8%B8%D9%85%D8%A9-%D8%A7%D9%84%D8%B9%D8%AF%D8%AF%D9%8A%D8%A9-%D9%88%D8%A7%D9%84%D8%AB%D9%86%D8%A7%D8%A6%D9%8A-%D9%84%D9%84%D9%85%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/memory-basics-cover-cover.webp)دليلالذاكرة للمبرمجين: كيف تعمل ال Stack وال Heapلماذا تتغير قائمة لم تطلب تغييرها، وكيف تتسرب الذاكرة في لغة فيها جامع قمامة Garbage Collector، بأمثلة حقيقية من عدة لغات.](https://arabroot.io/articles/%D8%A7%D9%84%D8%B0%D8%A7%D9%83%D8%B1%D8%A9-%D9%84%D9%84%D9%85%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D9%86-stack-%D9%88-heap-%D9%88%D8%A7%D9%84%D9%85%D8%A4%D8%B4%D8%B1%D8%A7%D8%AA/)[![](https://arabroot.io/content/images/2026/10/cpu-internals-cover-cover.webp)دليلكيف يعمل المعالج CPU من الداخل ولماذا يهمك ذلكنتابع تعليمة واحدة داخل نواة المعالج، ونرى لماذا يصبح نفس الكود أبطأ بعشر مرات عندما تغير ترتيب المرور على البيانات.](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D9%8A%D8%B9%D9%85%D9%84-%D8%A7%D9%84%D9%85%D8%B9%D8%A7%D9%84%D8%AC-cpu-%D9%85%D9%86-%D8%A7%D9%84%D8%AF%D8%A7%D8%AE%D9%84/)[![](https://arabroot.io/content/images/2026/10/sql-basics-cover-cover.webp)دليلأساسيات SQL للمطورين من SELECT إلى المعاملات Transactionsتتعلم SQL من الصفر على قاعدة بيانات صغيرة في PostgreSQL، من أول SELECT إلى المعاملات ومستويات العزل التي تحمي بياناتك.](https://arabroot.io/articles/%D8%A3%D8%B3%D8%A7%D8%B3%D9%8A%D8%A7%D8%AA-sql-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/api-design-cover-cover.webp)دليلكيف تصمم API لا تضطر إلى كسره بعد سنةمبادئ تصميم API يتطور دون أن يكسر التطبيقات التي تعتمد عليه، وكيف تختار الشكل المناسب لنظامك.](https://arabroot.io/articles/%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D9%88%D8%A7%D8%AC%D9%87%D8%A7%D8%AA-api-rest-%D9%88-grpc-%D9%88-graphql/)[![](https://arabroot.io/content/images/2026/10/local-llm-hardware-cover-cover.webp)دليلتشغيل نماذج اللغة على جهازك: كم ذاكرة وأي GPU تحتاج؟كيف تحسب الذاكرة التي يحتاجها النموذج، وماذا تتوقع من جهازك الحالي قبل أن تشتري بطاقة رسومية GPU.](https://arabroot.io/articles/%D8%AA%D8%B4%D8%BA%D9%8A%D9%84-%D9%86%D9%85%D8%A7%D8%B0%D8%AC-%D8%A7%D9%84%D9%84%D8%BA%D8%A9-%D8%B9%D9%84%D9%89-%D8%AC%D9%87%D8%A7%D8%B2%D9%83-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)

## سجل التحديثات

- أكتوبر 2026: كتابة المقال، وتنفيذ الأمثلة في Containers على x86-64 مع Python 3.13.8 وNode.js 22.20.0 و.NET SDK 10.0.100 وPostgreSQL 17.11.