کامپیوتر , مهندسی 170 بازدید

شاید شما نیز تاکنون در مورد نحوه انجام عملیات ریاضی در کامپیوترها که در سطح پایین صورت می‌پذیرد کنجکاو شده باشید. همه ما کمابیش می‌دانیم که رایانه‌ها عملیات ریاضیاتی را به روشی متفاوت از انسان‌ها اجرا می‌کنند، زیرا رایانه‌ها به روش دودویی (Binary) عمل می‌کنند. در این نوشته به بررسی عملیات ریاضیاتی در رایانه‌ها از طریق معرفی عملگرهای منطقی می‌پردازیم. شما می‌توانید در دنیای فیزیکی به جای عملگرهای منطقی گیت‌های متناظرشان را قرار دهید.

دقت کنید که برای درک نیمه دوم این راهنما باید با جبر بولی آشنا باشید و این موضوع در راهنمای حاضر معرفی نشده است. در این خصوص، می‌توانید این مطلب را مطالعه کنید.

Math

چرا باید از دودویی استفاده کنیم؟

این مقاله را با یک سؤال ساده شروع می‌کنیم. ما چرا به روش کنونی می‌شماریم؟ چرا از اعداد 1، 2، 3، 4، 5، 6،7، 8، 9 و 10 برای شمارش استفاده می‌کنیم؟ هر عددی که پس از این‌ها بیاید صرفاً با تکثیر رقم‌ها یا جابجایی آن‌ها بیان می‌شود.

برای نمونه چرا ما برای شمارش فقط از ارقام 1، 2، 3، 4، 5، 6، 7، و 10 استفاده نمی‌کنیم؟ شاید در ابتدا این مسئله یک انتخاب دلخواه به نظر برسد، اما اگر عمیق‌تر شویم یک دلیل احتمالی شاید این باشد که تعداد انگشت‌های دست انسان 10 تا است.

fingers

ما در هر دست خود 10 انگشت داریم و از این رو سیستم اعداد که استفاده می‌کنیم هم بر مبنای 10 رقم است. متأسفانه مدارهای الکترونیکی از این موهبت خدادادی برخوردار نیستند. این مدارها در هر زمان تنها یکی از دو حالت را می‌توانند نمایش دهند که به صورت حضور یا عدم حضور جریان در سیم‌ها نمایش می‌یابد. بنابراین ما در مدار الکتریکی با استفاده از 1 و 10 می‌شماریم.

اکنون یکی از سؤالات بدیهی این است که چرا 1 و 10؟ چرا 1 و 2 نباشد؟ برای پاسخ به این سؤال باید کمی توضیح دهیم.

اهمیت صفر

برای درک اهمیت صفر باید دو سیستم متفاوت شمارش یعنی روش مدرن هندو-عربی و شمارش با استفاده از اعداد رومی را با هم مقایسه کنیم.

  • سیستم هندوعربی: 10، 9، 8، 7، 6، 5، 4، 3، 2، 1
  • سیستم رومی: I، II، III، IV، V، VI، VII، VIII، IX، X

تفاوت مهم بین این دو سیستم در عدد آخر است. در سیستم رومی نماد خاصی برای عدد 10 وجود دارد. برای درک این تفاوت یک مثال ساده را بررسی می‌کنیم. در خط زیر روش نوشتن عدد 205 را در سیستم اعداد رومی مشاهده می‌کنید:

CCV

با این وجود، همین عدد در سیستم هندو-عربی به روش زیر نوشته می‌شود:

205 = 2 * 100 + 0 * 10 + 5 * 1

هیچ ارتباط بدیهی بین عدد و سیستم اعداد رومی وجود ندارد؛ در حالی که بین عدد 205 و موقعیت ارقام مختلف یک همبستگی مشخص وجود دارد.

این اهمیت صفر را مشخص می‌کند. صفر امکان تعریف جایگاه برای ارقام در سیستم اعداد را فراهم ساخته است.

اگر هنوز قانع نشده‌اید، به تفاوت‌های نمایشی بین سیستم اعداد رومی و هندو-عربی در ادامه توجه کنید:

Hindu-Arabic    Roman
100          C
1000         M

شمارش در سیستم دودویی

در ادامه روش شمارش در سیستم دودویی یا باینری را بررسی می‌کنیم. در این سیستم اعداد به ترتیب به صورت 00، 10، 01، 11 است. هر کدام از این ارقام یک بیت نامیده می‌شوند. به این اعداد، اعداد دودویی 2-بیتی گفته می‌شود.

مفهوم 2-بیت را نباید با دودویی اشتباه بگیرید. منظور از دودویی این است که ما برای هر موقعیت از ارقام 0 یا 1 استفاده می‌کنیم. تعداد بیت‌ها نشان‌دهنده تعداد ارقام هر عدد است. اینک باید مواردی را در مورد اجرای عملیات جبر بولی روی هر یک از اعداد دودویی یاد بگیریم. اگر می‌خواهید در مورد جبر بولی بیشتر بدانید، بهتر است ابتدا مقاله «منطق ترکیبی به زبان ساده» را مطالعه کنید. در ادامه تصویری از نتیجه سه عملیات متفاوت جبر بولی را مشاهده می‌کنید. این عملیات به نام گیت‌های منطقی نیز نامیده می‌شوند:

logic gates

logic gates

می‌توان همه گیت‌های منطقی فوق را با استفاده از مدارها و قطعات الکتریکی ساخت. در این نوشته قصد نداریم به شیوه ساخت این مدارها بپردازیم، چون خارج از حیطه این مقاله است. در لینکی که در بخش قبل ارائه کردیم می‌توانید در این مورد بیشتر مطالعه کنید.

آنچه فعلاً باید بدانیم این است که چگونه می‌توانیم هر یک از این گیت‌های منطقی را در کد نمایش دهیم. برای نمونه در جاوا اسکریپت از عملگرهای زیر استفاده می‌کنیم:

AND &
OR |
XOR ^

این عملگرها به نام عملگرهای سطح بیتی نیز نامیده می‌شوند و در ادامه در مورد آن‌ها بیشتر توضیح می‌دهیم. عملگرهای سطح بیتی را می‌توان مشابهی برای گیت‌های منطقی برشمرد. در این خصوص می‌توانید در مقاله «منطق شرطی (Conditional Logic) در جاوا اسکریپت» بیشتر بخوانید.

ساخت یک ماشین جمع کننده دودویی

بدین ترتیب ما تا به اینجا قواعد مقدماتی مورد نیاز برای جبر بولی را داریم. اینک چگونه می‌توانیم دو عدد دودویی را با هم جمع بکنیم؟ به جدول زیر توجه کنید:

ما نتیجه را با استفاده از 2 بیت ارائه کرده‌ایم. بیت راست، بیت مجموع است و بیت سمت چپ به نام بیت انتقالی (carry bit) نامیده می‌شود.

اگر جدول را به دو جدول مجزا، یکی برای نمایش بیت مجموع و دیگری برای نمایش بیت انتقالی تقسیم کنیم به شکل زیر درمی‌آید:

ما اینک همه چیزهایی که برای عمل جمع در سیستم دودویی مورد نیاز است را می‌دانیم. در ادامه باید روشی برای نمایش دو جدول فوق برحسب کد پیدا کنیم.

ترکیب مدارها و کد

اینک آنچه را که تا اینجا آموخته‌ایم یادآوری می‌کنیم:

  1. ما سه گیت منطقی متفاوت و عملگرهای متناظرشان را در جاوا اسکریپت می‌شناسیم.
  2. ما چگونگی افزودن دو عدد دودویی به صورت دستی با استفاده از جدول‌های جمع و انتقالی که قبلاً اشاره کردیم را می‌دانیم.

ما باید روشی برای بیان 2 جدول در 1 جدول پیدا کنیم. بدین ترتیب دو عدد دودویی را با استفاده از عملگرهای بیتی متفاوت جمع می‌کنیم. در ادامه جدول‌های جمع و انتقالی را به طور مجزا بررسی می‌کنیم. ابتدا جدول جمع را در نظر بگیرید.

اگر به ابتدای این مقاله بازگردید، می‌بینید که سه جدول گیت منطقی یعنی AND ،OR و XOR را داریم. اگر مقادیر هر سلول جدول جمع و جدول XOR را مقایسه کنیم، درمی‌یابیم که مطابقت دقیقی وجود دارد.

بنابراین اگر دو عدد دودویی به صورت 0 و 1، وجود داشته باشد، با انجام کارهای زیر بیت مجموع به دست می‌آید. شما می‌توانید وضعیت را در پنجره dev tools در مرورگر امتحان کنید.

سپس بیت انتقالی را در نظر بگیرید. همین کار را انجام دهید و با مراجعه به بخش‌های قبلی، سه جدول منطقی را که داریم در نظر بگیرید و آن‌ها را با جدول انتقالی مقایسه کنید. درخواهید یافت که جدول‌های انتقالی مطابقت دقیق دارند. این وضعیت را در کد به صورت زیر بیان می‌کنیم:

بنابراین می‌توانیم دو عدد 1 بیتی را با هم جمع کنیم و بیت جمع و بیت انتقالی را با استفاده از ترکیبی از یک گیت AND و گیت XOR به دست آوریم. این گیت را نیم‌جمع‌کننده می‌نامیم.

گیت نیم‌جمع‌کننده را به صورت زیر کدنویسی می‌کنیم. در کد زیر اعداد 1010 و 0101 را با هم جمع می‌کنیم. در مبنای 10 این دو عدد به ترتیب 10 و 5 نامیده می‌شوند.

بنابراین می‌دانیم که این کار را چگونه به صورت دستی انجام دهیم و از این رو هر ستون اساساً عملیات یکسانی به صورت یعنی جمع 1 و 0 است. می‌دانیم که نتیجه این جمع 1 است. کد آن به صورت زیر است:

اگر کد فوق را اجرا کنیم، باید نتیجه 15 را ببینیم که همان چیزی است که انتظار داریم.

با این وجود، یک موقعیتی وجود دارد که هنوز کد آن را ننوشته‌ایم. اگر به عقب بازگردید و به دو عدد دودویی که جمع کردیم نگاه کنید، متوجه خواهید شد که هرگز در هیچ کدام از جمع‌ها، انتقال بیت صورت نگرفته است. اگر لازم باشد که دو عدد زیر را با هم جمع کنیم چه اتفاق می‌افتد؟

دقت کنید که کد فوق نمی‌تواند پاسخ صحیح را بدهد، زیرا ما هرگز انتقال از یک ستون به ستون بعد را انجام نداده‌ایم. در واقع مفهوم ما از نیم‌جمع‌کننده نمی‌تواند این کار را انجام دهد، زیرا نیم‌جمع‌کننده تنها مسئول دو بیت ورودی A و B است. ما به چیزی نیاز داریم که سه بیت ورودی را مدیریت کند.

مدار تمام‌ جمع‌ کننده

مدار تمام‌جمع‌کننده در پس‌زمینه تنها ترکیبی از دو نیم‌جمع‌کننده است که با گیت OR به صورت سری به هم اضافه شده‌اند. برای نمایش تمام‌جمع‌کننده برحسب برنامه‌نویسی، قصد داریم زمانی را صرف بررسی کامل آن بکنیم.

شاید توضیح چگونگی کارکرد این مدار کمی دشوار باشد. ما روش کارکرد این مدار را با استفاده از ستون دوم از راست جمع که اینک روی آن کار می‌کنیم توضیح می‌دهیم و با این توضیح، درک آن کمی آسان‌تر می‌شود.

تصویر فوق نسخه حاشیه‌نویسی شده‌ای از ستون دوم از راست برای جمع زیر به دست می‌دهد:

  • دو ورودی به نیم‌جمع‌کننده در مدار، دو مقداری در ستون‌ها هستند که هر دو 1 هستند.
  • ورودی‌ها به دومین نیم‌جمع‌کننده در مدار مجموع نیم‌جمع‌کننده قبلی هستند و از ستون چپ انتقال می‌یابند.
  • انتقالی از ستون فعلی و انتقالی از ستون سمت راست هر دو به گیت OR بعدی وارد می‌شوند. نتیجه این محاسبه OR انتقالی از این ستون به ستون چپ است.

ما چرا آن گیت OR را در انتها داریم؟ چرا نیم‌جمع‌کننده دیگری در آنجا نداریم؟ البته این کار ممکن است اما گیت OR بسیار ساده‌تر است. دلیل این که گیت OR برای نیازهای ما کافی است این است که هیچ مجموعه ورودی دیگری برای دو نیم‌جمع‌کننده اول وجود ندارد که دو 1 به عنوان ورودی‌های گیت OR ارائه کند. شما می‌توانید با بررسی وضعیت‌های مختلف درستی این گزاره را خودتان امتحان کید.

اینک تمام‌جمع‌کننده را در کد زیر نمایش می‌دهیم:

احتمالاً متوجه چند تفاوت در روش سازماندهی کد برای نیم‌جمع‌کننده و تمام‌جمع‌کننده شده‌اید و تفاوت عملکردی اصلی متغیری است که به طور مداوم با بیت انتقالی از ستون قبلی بازنویسی می‌شود.

ضمناً در انتهای عملیات، carryIn === 1 را برسی می‌کنیم تا مطمئن شویم که انتقالی از ستون سمت چپ را از دست نداده‌ایم. همچنین یک تابع کمکی کوچک به نام convert2Decimal اضافه می‌کنیم تا بتوانیم نتیجه را در مبنای 10 ببینیم.

اگر می‌خواهید در مورد این وضعیت به صورت فیزیکی تصوری داشته باشید، در تصویر زیر هر آن چه را تاکنون مطرح کردیم، جمع‌بندی نموده‌ایم.

محاسبه هر ستون نیازمند یک تمام‌جمع‌کننده است. از این رو جمع کننده 8 بیتی ما از 8 آدرس کامل تشکیل یافته است. بیت‌های نقلی خروجی از هر یک از این جمع‌کننده‌ها به عنوان بیت نقلی ورودی برای جمع کننده بعدی استفاده می‌شوند.

مجموع بیت‌ها از هر ستون انتقال می‌یابند و با هم تجمیع می‌شوند تا یک خروجی جمع 8 بیتی تولید کنند.

ساخت یک ماشین تفریق باینری

اینک که با روش جمع دودویی در کدهای نرم‌افزاری آشنا شدیم، نوبت آن رسیده است که تفریق دودویی را بررسی کنیم. ما تفریق را به صورت تفریقی برای یک جمع کننده 8 بیتی که در بخش قبلی ساختیم تعریف می‌کنیم. ماشین تفریق باینری که در این راهنما می‌سازیم دو محدودیت دارد:

  1. تنها می‌تواند اعداد 8 بیتی را مدیریت کند.
  2. تنها اعدادی با نتیجه مثبت را از هم تفریق می‌کند.

این محدودیت‌ها جهت کاهش پیچیدگی مسئله با مقاصد آموزشی اعمال شده‌اند. ابتدا به روش معمول تفریق دستی توجه کنید:

با توجه به مقادیر فوق می‌بینیم که اصطلاح‌های خاصی بدین منظور وجود دارد. برای اجرای تفریق فوق باید برخی اعداد را از چپ قرض بگیریم. انجام این کار روی اعداد دودویی کمی پیچیده است. در ادامه یک ترفند ریاضیاتی که در کد استفاده می‌شود را ارائه کرده‌ایم.

زیبایی این ترفند آن است که دیگر به قرض کردن عدد در هیچ عملیاتی نیاز نداریم. مهم‌ترین بخش مراحل فوق قسمت زیر است:

999 — 176

نتیجه این تفریق مکمل 9 برای عدد 176 نامیده می‌شود.

ما در سیستم دودویی به مکمل 1 نیاز داریم که به صورت زیر نمایش می‌یابد (در این عملیات بازنمایی دودویی از 255 استفاده می‌شود که بزرگ‌ترین عدد ممکن با 8 بیت است):

اگر به دقت به عدد دودویی در سمت راست تفریق و نتیجه تفریق نگاه کنید، می‌بینید که نتیجه دقیقاً متضاد سمت راست است، یعنی هر کجا 1 بوده 0 شده و هر کجا 0 بوده 1 شده است.

یک روش سریع برای شبیه‌سازی این وضعیت در جاوا اسکریپت استفاده از عملگر ~ است. اما مشکلی در بازنمایی جاوا اسکریپت از اعداد دودویی منفی و مثبت وجود دارد.

جاوا اسکریپت همه اعداد را تنها به صورت اعداد اعشاری نشان می‌دهد. البته جاوا اسکریپت در سازوکار درونی خود این اعداد را در موارد مقتضی به اعداد صحیح تبدیل می‌کند و پس از انجام محاسبات مورد نیاز دوباره به حالت اعشاری بازمی‌گرداند. جاوا اسکریپت از دو نسخه علامت‌دار و بی‌علامت برای بازنمایی درونی اعداد استفاده می‌کند.

اگر خواهیم از عملگر ~ استفاده کنیم، باید تنها از اعداد صحیح بی‌علامت استفاده کنیم.

از آنجا که جاوا اسکریپت یک زبان با نوع‌بندی سست (loosely typed) است، می‌توانیم اعداد صحیح علامت‌دار را به اعداد صحیح بی‌علامت تبدیل می‌کنیم و سپس آن‌ها را مورد استفاده قرار دهیم. عملگری که استفاده می‌کنیم عملگر >>> است.

به تابع convert2Binary خود که قبلاً استفاده کردیم، کدی به صورت زیر اضافه می‌کنیم:

به تابع فوق دو مورد اضافه می‌کنیم:

  1. یک پارامتر اضافی به نام padding اضافه می‌کنیم. بدین ترتیب می‌توانیم تعداد رقم‌هایی که هنگام تبدیل به دودویی استفاده می‌شوند را کنترل می‌کنیم. با استفاده از این بخش می‌توانیم به طور موقت عدد 256 را به عنوان یک مرحله میانی مدیریت کنیم.
  2. پارامتر num را پیش از تبدیل شدن به دودویی به دست می‌آوریم.

در ادامه عملگرهایی که قصد داریم بار دیگر اجرا کنیم را بررسی می‌کنیم.

خطوط عمودی نشان‌دهنده کران‌های هر یک از مراحل هستند. برای نمونه مرحله a تنها روی اعداد درون پرانتزها اجرا می‌شود و همین طور تا آخر. این ترتیب عملیات در کد نیز رعایت شده است. در ادامه کد تابع تفریق را می‌بینید:

در ابتدای تابع یک رویه مدیریت خطای بسیار ساده وجود دارد. ضمناً وقتی عدد 256 را به تابع convert2Binary ارسال می‌کنیم یک فاصله‌گذاری 9 نیز می‌فرستیم. دلیل این مسئله آن است که 255 بزرگ‌ترین عددی است که می‌توان با 8 بیت نشان داد و از این رو در این مورد به 9 بیت نیاز داریم.

یک مرحله دیگر هم وجود دارد که باید ارائه کنیم. مرحله d الزام می‌کند که رقم نقلی خروجی از تفریق را داشته باشیم. چگونه می‌توانیم این رقم نقلی را از تفریق خارج کنیم؟

اگر مجدداً به گیت‌های منطقی خود فکر کنیم، متوجه می‌شویم کد منطقی یک شبیه‌سازی از عملیات تفریق است.

گیت XOR دقیقاً آن چیزی که نیاز داریم را در اختیار ما قرار می‌دهد. از آنجا که تنها تفریقی که انجام می‌دهیم، رقم خروجی بین 256 (با نمایش دودویی 100000000) و عددی کمتر از 256 است، از این رو هرگز نباید نگران قرض گرفتن عددها باشیم. در ادامه کد آنچه که convert2Binary می‌نامیم را می‌بینید:

همچنین می‌خواهیم تابع binaryAdder را که در بخش قبلی توسعه دادیم ببینیم. از آنجا که امکان اجرای جمع یا تفریق را با استفاده از جمع کننده 8 بیتی یکسانی داریم، باید بین زمان‌هایی که می‌خواهیم جمع یا تفریق کنیم تمایزی قائل شویم. ما از یک عدد به عنوان پارامتر به این منظور استفاده می‌کنیم. بدین ترتیب 0 به معنی جمع و 1 به معنی تفریق خواهد بود.

در نهایت کد کامل ما به صورت زیر است:

در ادامه آنچه را که برای ساخت بازنمایی کد انجام داده‌ایم را مشاهده می‌کنید. این بازنمایی دقیقی نیست؛ اما تقریباً همان کارکرد را به روشی مشابه اجرا می‌کند.

دقت کنید که ورودی B از طریق کادر مکمل 1 که به سیگنال SUB وارد می‌شود گذر می‌کند. سیگنال SUB یا 0 و یا 1 است. اگر 0 باشد ورودی B معکوس نمی‌شود و برعکس. ما این وضعیت را با استفاده از پارامتر operationBit که به تابع binaryAdder ارسال می‌کنیم شبیه‌سازی کرده‌ایم.

سخن پایانی

بدین ترتیب موفق شدیم روش اجرای عملیات ریاضی از سوی رایانه‌ها را درک کنیم. در این مطلب با بازنمایی یک جمع کننده دودویی به صورت کد آشنا شدیم. جمع کننده دودویی ما اعمال جمع و تفریق اعداد را با شبیه‌سازی گیت‌های منطقی اجرا می‌کند که البته ساده‌سازی‌های زیادی روی آن صورت گرفته است.

شما می‌توانید تلاش کنید تا شیوه بازنمایی ضرب و تقسیم را با استفاده از گیت‌های منطقی در کدهای جاوا اسکریپت شبیه‌سازی کنید. توجه داشته باشید که این دو عملیات بسیار پیچیده‌تر هستند؛ اما به هر حال برای علاقه‌مندان به این موضوع، یک تمرین جذاب محسوب می‌شوند.

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

آیا این مطلب برای شما مفید بود؟

نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *