الفريق العربي للبرمجةأرشيف المنتديات · 2000 – 2023
نسخة أرشيفية للقراءة فقط — التسجيل والمشاركة مغلقان، والمحتوى محفوظ كما كان.

ترميز الأعداد الكسرية (وفق تجارب مخبرية p: )

بدأه مصطفى 36a2 في 14 ديسمبر 2013 · 6 رد · 1,228 مشاهدة · في لغة C و ++C
مشاركة: واتساب X فيسبوك تيليجرام
#1 صاحب الموضوع

السلام عليكم ورحمة الله وبركاته
ليكن لدينا الكود التالي

#include <stdio.h>
#include <stdlib.h>

int main()
{
    printf("Hello world!\n");
    printf("%f",1.5);
    return 0;
}

كيف يتم تخزين الأعداد العشرية والتعامل معها من قبل المعالج ؟
إليك ما يلي
لطباعة العدد 1.5
قام البرنامج بدفع قيمتين للدالة كوسيط كما يلي
الأولى 0 ( سنتحدث عنها في آخر المقالة )
والثانية 3FF80000
فماذا تعني كل واحدة ؟
نحن طلبنا العدد 1.5 فأين هو ؟
القيمة الثانية 3ff80000 هي

0011 1111 1111 1000 0000 0000 0000  0000

فأين ماذا من كيف ؟ ×_×

لنبدأ .... (يمكنك متابعة الشرح بنفسك إن كان عندك برنامج تنقيح مثل ollydbg ابحث عن HelloWorld وتابع البرنامج من هناك .. فالرحلة ممتعة)

لنجرب تغيير أول واحد من اليمين .. يعني بدل 8 سنضع صفر
!!!
قام البرنامج بطباعة 1 بدل 1.5
لنجرب الآن أن نغير الواحد الثاني
بدل F وهي 1111سنضع1110 أي وهو E
ثم سنجرب C ثم سنجرب 8
من أجل E طبع 0.5
ومن أجل C طبع 0.125
ومن أجل 8 أعطى 0.007813
إذا سنتوقف عند C ... ونتابع استنتاجها لاحقا
لو وضعنا 80000000 سيعطينا -0 ويبدو أن آخر بت هو بت الإشارة .. هذا جيد
للتأكد نضع BFF80000 بدلا من 3FF80000 أي نغير فقط آخر بت والنتيجة -1.5
هذا جيد
لو وضعنا 00C00000 فسيعطي صفرا :(
لكن لا بد أن هناك أرقام أخرى بعد الفاصلة .. لذا سنغير %f في الكود إلى %.30f
بمجرد إزالة 3 ووضع صفر فالعدد الناتج يكون صفراً ..
لنحاول ألا نقترب من البتات اليسارية كثيراً وفق ما يلي

3        F        F        8        0        0        0        0
0011    1111    1111    1000    0000    0000    0000    0000

كل البتات التي نتكلم عنها سنعدها من اليسار لليمين
الأول للإشارة
من الثاني إلى التاسع تسبب مشاكل :)
من العاشر إلى الثالث عشر سنختبرها الآن
لنجرب الآن

3F780000=0011 1111 0111 1000 0000 0000 0000 0000
0.005859375000
لنجرب
3FB80000=0011 1111 1011 1000 0000 0000 0000 0000
0.093750000000
لنجرب
3FD80000=0011 1111 1101 1000 0000 0000 0000 0000
0.375000000000
لنجرب
3FE80000=0011 1111 1110 1000 0000 0000 0000 0000
0.750000000000
وطبعا جربنا
3FF00000=0011 1111 1111 0000 0000 0000 0000 0000
1.000000000000

ما المعنى من كل هذا ؟؟
لنتابع الآن بزيادة واحدات بدلا من حذفها

3FFC0000=0011 1111 1111 1100 0000 0000 0000 0000
1.75

3FFE0000=0011 1111 1111 1110 0000 0000 0000 0000
1.875000

هل يمكننا اعتبار البت الخامس عشر يساوي 0.125
لنجرب ما يلي
لدينا

3FF00000=0011 1111 1111 0000 0000 0000 0000 0000
1.000000000000
هل هذا يعني أن
3FF20000=0011 1111 1111 0010 0000 0000 0000 0000
تساوي
1.125

!! نعم
ماذا عن

3FF10000=0011 1111 1111 0001 0000 0000 0000 0000

هل تساوي 1.0625
(0.0625=0.125 مقسومة على 2)
!! نعم !!
إلى أي حد يا ترى ؟

3FF00001=0011 1111 1111 0000 0000 0000 0000 0001

المفترض أن يساوي 1مقسوم على 2^20
أي 0.00000095367431640625
الناتج
1.000000953674316400000000000000
طبعاً المشكلة في  في دالة العرض على أي حال ..
والآن لنعد إلى البتات الأوائل من 2 إلى 12
نعلم أن

3FF00000=0011 1111 1111 0000 0000 0000 0000 0000

يعطي قيمة الواحد
ماذا عن

3FE00000=0011 1111 1110 0000 0000 0000 0000 0000

يعطي 0.5 !
هل هذا يعني أن البت 12 وزنه 0.5
لنتابع

3FC00000=0011 1111 1100 0000 0000 0000 0000 0000 = 0.125
3F800000=0011 1111 1000 0000 0000 0000 0000 0000 = 0.0078125

بدأت الأمور تخرج عن السيطرة ! فالنتيجة السابقة هي 1 مقسوم على 2^7
قبل المتابعة علينا التأكد من آخر استنتاج

3FC00000=0011 1111 1100 0000 0000 0000 0000 0000 = 0.125
3FF00000=0011 1111 1111 0000 0000 0000 0000 0000 = 1.000
----------------------------------------------------------
3FD00000=0011 1111 1101 0000 0000 0000 0000 0000 = 0.625 ؟؟

خطأ !! = 0.25
أي كأننا ضربنا 0.125 بـ 2!!
لاحظ أن

3FF00000=0011 1111 1111 0000 0000 0000 0000 0000 = 1.000
بينما
3FE00000=0011 1111 1110 0000 0000 0000 0000 0000 = 0.500

أي كأننا قسمنا على 2
فهل تعني البتّات ال12-11-10-9 الضرب بقوى العدد 2!
عندما يكون البت واحداً نضرب بـ2 !!
ولكن انظر ::
3FF

00000=0011 1111 1111 0000 0000 0000 0000 0000 = 1.000 /2 =0.5
3FE00000=0011 1111 1110 0000 0000 0000 0000 0000 = 0.500 /2 =0.25 /2 =0.125
3FC00000=0011 1111 1100 0000 0000 0000 0000 0000 = 0.125 /2 =0.0625 /2 =0.3125 /2= 0.15625 /2 =0.078125
3F800000=0011 1111 1000 0000 0000 0000 0000 0000 = 0.078125

وضحت الفكرة !!
البت 12 يكافئ الضرب بـ2 وهي 2 مرفوعة لـ1
بينما يكافئ البت 11 الضرب بـ 4 وهي 2 مرفوعة لـ2
هكذا .. البت 10 للضرب بـ  16 وهي 2 مرفوعة لـ4
والبت 9 للضرب بـ 256 وهي 2 مرفوعة لـ8
فهل هذا يعني أن

3F000000=0011 1111 0000 0000 0000 0000 0000 0000 = 0.00003051757812500

أي نقسم آخر جواب على 256
النتيجة
0.00003051757812500
بالفعل !!صحيح :)
لنجرب إلى أي مدى استنتاجنا صحيح ؟ ولكن يجب أن نحذر من الانحدار الكبير للقيم .. والأخلاق

3E000000=0011 1110 0000 0000 0000 0000 0000 0000 = 0.00003051757812500/65536=0.0000000004656612873077392578125
==>0.000000000465661287307739260000 مقبول
3C000000=0011 1100 0000 0000 0000 0000 0000 0000 = 0.0000000004656612873077392578125/4294967296=1.0842021724855044340074528008699e-19

اظن أن هذه الدقة كبيرة بما يكفي !!
0.000000000000000000108420217249 ممتاز
لنتابع

38000000=0011 1000 0000 0000 0000 0000 0000 0000 = 1.0842021724855044340074528008699e-
19/18446744073709551616=5.877471754111437539843682686111e-39

يا إلهي !! الآن عرفت كيف يمكن لشيء كهذا أن يتحمل قيماً كبيرة إلى هذ الحد !!
فهو أصلاً يحتوي هذه القيم الكبيرة بشكل أو بآخر ..
مثلاً لو قلت لك .. إذا رفعت إبهامي فهذا يعني مليون وإذا رفعت السبابة فهي تعني مليار مليار
فيمكنني باستخدام اصبعين فقط أن أرمّز 4 أرقام كبيرة جداً .. (طبعاً مع دقّة سيئة جدّاً)
لنتابع :)
ولكن علينا تغيير الكود من %.30fإلى %g

30000000=0011 0000 0000 0000 0000 0000 0000 0000 =1.72723371101888892507727037256e-77

ويظهر على الشاشة
1.72723e-077 وهذا شيء مذهل :)

10000000=0010 0000 0000 0000 0000 0000 0000 0000 =1.4916681462400413486581930630925e-154

1.28823e-231
ماذا بقي !!

00000000=0000 0000 0000 0000 0000 0000 0000 0000 =1.1125369292536006915451163586661e-308

يطبعها صفراً
ولكن ماذا عن البت الثاني ؟

40000000=0100 0000 0000 0000 0000 0000 0000 0000

غريب ! النتيجة 2 :D
طيب هل يمكنك استنتاج قيمة

60000000=0110 0000 0000 0000 0000 0000 0000 0000

تذكر 2 مضروبة بــ 2^512 :)
يعني

2^513  ~=2.68156e+154
ممتاز !!
إذا يمكننا اعتبار ما يلي:
إذا كانت كل البتات أصفار فإن :
العدد يساوي
1.1125369292536006915451163586661e-308
وهو تماماً 2 مرفوعة للقوة السالبة 1023
2^(-1023)
أما إن كان البت الثاني من اليسار واحداً
فإن العدد يساوي
2+2^(-1023)
ونعتبره تقريباً
2
أنا سعيد جدّاً في هذه اللحظة .. :)
لنتابع
لم يبق شيء :D
لنتوقّف إذا ونراجع استنتاجاتنا
يتم ترميز الأعداد داخل متغير من نوع float بالشكل التالي
SDPP PPPP PPPP NNNN NNNN NNNN NNNN NNNN
S  الإشارة , صفر تعني موجب , وواحد تعني سالب
D  إذا كانت واحد نضيف 2 إلى العدد , وإن كانت صفر نعتبر أن العدد لدينا هو 2 مرفوعة للقوة 1023 سالبة
P  تؤدي إلى ضرب العدد بـ2 مرفوعة إلى القوة (2 مرفوعة إلى القوة رقم البت)
أول بت P من اليمين يؤدي إلى ضرب العدد بـ 2 مرفوعة إلى القوة (2مرفوعة إلى القوة 0) أي 2
الثاني من اليمين يؤدي للضرب بـ4
الثالث 16
وهكذا .. 256..65536..4294967296..18446744073709551616..
3.4028236692093846346337460743177e+38
1.1579208923731619542357098500869e+77
والأول من اليسار
1.3407807929942597099574024998206e+154

N تؤدي إلى جمع قوى العدد2 السالبة إلى الرقم حسب مكان البت
الأول من اليسار القوة 1 السالبة أي 0.5
الثاني من اليسار القوة 2 السالبة أي 0.25
وهكذا
0.125 - 0.0625 - 0.03125 - 0.015625 - .... - 0.00000095367431640625
وهكذا نكون قد أنهينا فصفصة عظام ومفاصل ترميز الأعداد الكسرية

ونكتشف في النهاية أن الترميز جميل ومتقن , ولكن  البرمجة ليست في الترميز ,
بل في الدالة printf التي تقوم بتحويل الترميز الثنائي هذا إلى قيمة مقروءة من قبل حضرتك
بل لنتكلّم بشكل أوسع ..

من أهم ميزات معالجات الـ32 بت أنها أتاحت لك وبشكل هاردويري أن تقوم بجمع عددين يستخدمان ترميز معقّد كهذا !!
تخيل كم توفّر عليك التعليمة FADD  من عمليات !!
حاول التفكير في طريقة لجمع رقمين بهذا الترميز .. حاول كتابة برنامج يقوم بذلك ..
لتعرف حجم التطوير الكبير في معالج إحدى تعليماته FADD :)

float x=1.5;float y=1.5;float z=x+y;

لو كانت عملية الجمع السابقة تحدث برمجيّاً , فعليك نسيان لعبة Pro Evolution Soccer ونسيان Counter Strike و برامج التصميم مثل maya وأخواتها .. والاكتفاء بماريو على كونسول سوداء ملوّنة :)

أرجو أن تكون الرحلة ممتعة لك كما كانت ممتعة لي :)
 بالمناسبة استخدمت gcc للترجمة وCode::blocks كبيئة وطبعاً للتنقيح وتغيير قيمة الوسيط المرر لـ printf استخدمت الصديق الصدوق olly dbg إن كان ذلك يهمّ أحداً ..

أخيراً يجب أن أذكر أمراً كنت قد تغاضيت عنه ..
وهو القيمة الأولى التي يتم تمريرها كجزء من العدد وكانت قيمتها صفر طوال مدة العرض
هذه القيمة ذات 32 بت جميعها تكملة لبتّات N الخاصة بحفظ قيمة الرقم وهي لزيادة الدقة
أما سبب التغاضي عنها فهو أنني من المفترض أنني أتعامل مع float وليس مع double !!
ولم أفهم سبب التعامل مع float دوماً كـ8بايت كاملة
حتى عبارة مثل هذه

printf("%f",1.5f);

تتسبب في دفع 8بايت للمكدس ..
على أي حال هي لزيادة الدقة .. وهذا جيد دوماً :P ..
(تمت كتابة المقالة in real time لذلك سامحونا للأخطاء المطبعية )

 

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

تم تعديل هذه المشاركة بواسطة مصطفى 36a2 في 15 ديسمبر 2013 في 20:22

1
#2

ما تتكلم عنه هو ترميز الأرقام العشرية و هو امر يعود للمعالج فيمكن ان المترجم لدية مكتبة run-time خاصة بالعمليات العشرية و لا يستخدم المعالج (توجد اسباب لهذا الامر و لكنها قليلة).

 

فى اغلب المعالجات يتم إستخدام IEEE 754 standard و يوجد منه اصدارين الاول بتاريخ 1985 و توجد تعديلات تمت عليه تم وضعها فى نسخة 1987 و الأخير بتاريخ 2008.

 

التحدث فى هذا الامر يطول و إن اردت يمكنك مراجعة هذه الصفحه لترميز الـ float-point بحجم 32بت (تسمي single-precision) و من خلالها يمكن الوصول للاحجام الاخرى.

 

ايضا يمكنك الرجوع لهذه المقالة التى تحتوى على عمليات على مستوى البت للأنواع الصحيحه و العشرية مثل log2 و ceil و غيرها.

 

 

و الله ولي التوفيق

تم تعديل هذه المشاركة بواسطة C++er في 15 ديسمبر 2013 في 16:36

2

مدونتي: C++ Tips and Tricks

#3

وعليكم السلام و رحمة الله و بركاته 

كانت الرحلة ممتعة جدا في عالم ترميز الأرقام الكسرية أخي الكريم مصطفى

فهمت بشكل مقبول كل المقطع الذي قبل هذا

 

مصطفى 36a2 كتب:

10000000=0010 0000 0000 0000 0000 0000 0000 0000 =1.4916681462400413486581930630925e-154

1.28823e-231
ماذا بقي !!

00000000=0000 0000 0000 0000 0000 0000 0000 0000 =1.1125369292536006915451163586661e-308

يطبعها صفراً

 

 

هو و الذي بعده ×_× لم أفهمه 

40000000=0100 0000 0000 0000 0000 0000 0000 0000

فكيف لذلك أن يساوي 2 فأنا أعلم أن 2 في النظام الثنائي هي 10 و ليس كل هذا فلماذا يطبع لنا 2؟

و فيما يخص الترميز لعدد من نوع float أنا أقصد SDPP PPPP PPPP PPPP NNNN NNNN NNNN NNNN NNNN لماذا تختلف عمليات الانتقال بين واحدة لأخرى من كل نوع؟ أي الانتقال من N إلى N ليس كالانتقال من P إلى P؟ و D أيضا لما لم تتكرر في بقية الترميز؟ و هل هذاالترميز فقط خاص بالمتغيرات من نوع float أم لا؟ و أيضا لماذا تم تقديم هذا الترميز على 36 بت أهذا عائد الى أن كل تلك الرموز ضرورية و لا يجب المساس بها؟

و في الأخير أخي الكريم مصطفى أنا أعلم أنني أكثرت من الأسئلة ولكنني لم أتمكن من فهمها جيدا...لذلك أرجو المعذرة على هذا

و بالتوفيق شكرا لك على هاته الرحلة الماتعة ^_^  

تم تعديل هذه المشاركة بواسطة tantie L في 15 ديسمبر 2013 في 20:47

ابتسم للدنيا تبتسم لك و افعل الخير و انساه و لا تنسى أي أمر سيء قمت به حتى لا تكرره


لا تنسونا من صالح دعائكم أعانكم الله و ثبتكم على سواء السبيل


:)  :lol:  ^_^ 

#4

جزاك الله خيراً , روابط مفيدة جداً ..

اقتباس

ايضا يمكنك الرجوع لهذه المقالة التى تحتوى على عمليات على مستوى البت للأنواع الصحيحه و العشرية مثل log2 و ceil و غيرها.

مقالة رائعة !! هل هذا الشخص Sean Eron Anderson هو من اخترع هذه الطرق الذكيّة !! سبحان الله ! وجائزة لمن يوجد أي خطأ !

شكراً جزيلاً لك ..

اقتباس

ما تتكلم عنه هو ترميز الأرقام العشرية و هو امر يعود للمعالج فيمكن ان المترجم لدية مكتبة run-time خاصة بالعمليات العشرية و لا يستخدم المعالج

لا بد أن برامجه ستكون بطيئة بشكل ملحوظ ..

 

وفقك الله

#5
اقتباس
لا بد أن برامجه ستكون بطيئة بشكل ملحوظ ..

على العكس تكون سريعه، لأن فى مثل هذه الحالات يتم إستخدام Fixed-point و ليس Float-point، و العلميات على Fixed-point مثلها مثل العمليات على الأعداد الصحيحه مع بعض الفروق البسيطة، يكفي انك لن تحتاج لإجراء normalization للرقم مع كل عملية.

 

 

و الله ولي التوفيق

مدونتي: C++ Tips and Tricks

#6

@tantie L أهلاً بك ..

نعم هذا الترميز خاص بـ float  البت الثاني من اليسار خاص جدّاً فهو يضيف 2 للعدد .

البتّات المعلّمة بـ N خاصة لترميز الجزء الكسري .. وكما تعلمين في الأعداد الصحيحة تكون البتات موزونة 1 ثم 2 ثم 4 ثم 8 وهكذاً .. أما هنا 0.5 ثم 0.25 ثم 0.125 ثم 0.0625 وهكذا .. (قوى 2 السالبة )

أما بتّات P فهي للقوّة .. وهي منفصلة تماماً عن N .. فعندما نضع 1 في أول بت P من اليمين فهذا يعني ضرب العدد بـ 2^1 والثاني يصرب العدد بـ 2^2 والثالث 2^4 والرابع 2^16 وهكذا ..

نعم هذا الترميز خاص بالأعداد الكسرية وهو يختلف عن طريقة ترميز الأعداد الصحيحة بشكل كبير ..

شكراً لمرورك ..

بالتوفيق :)

#7

@C++er

fixed-point هذا يعني تصغير الحدود وزيادة الدقة , كما أن ترميزها بسيط  .. ظننت أنها ستستعمل single precision أيضاًُ ..

بالمناسبة لمن لا يعرف fixed point

فهي مجرد إضافة قسم صحيح إلى قسم كسري

مثلاً لنأخذ 1 بايت للقسم الصحيح .. ويكون أكبر رقم صحيح موجب 127وأكبر رقم صحيح سالب هو 128- وترميزه هو الترميز المعروف

و مثلاً 1  بايت للقسم الكسري .. وتكون أوزان البتات فيه هي قوى 2 السالبة 0.5 ثم 0.25 ثم 0.125 وهكذا .. من اليمين إلى اليسار .. إلى 0.00390625

مثلاً

0000 0001 1000 0000

تمثّل 1.5 (8 بت للقسم الصحيح على اليسار , و8 بت للقسم الكسري على اليمين )

(صحح لي إن كنت مخطئاً أخي C++er .. :) )

بارك الله فيكم

تم تعديل هذه المشاركة بواسطة مصطفى 36a2 في 15 ديسمبر 2013 في 22:30

مواضيع مشابهة