حدود ۷ ماه پیش، مسابقه سوم سیسوگ رو برگزار کردیم و کلی نکته در مورد خواندن رشتههای ورودی را بررسی کردیم. فکر کردم که بد نیست یک چالش جدید داشته باشیم! البته چالشها هیچ وقت بی حاشیه نیستند مثل سه مسابقه قبل، در واقع مسئله آینه که کنار هم چیزهای جدیدی یاد بگیریم نه اینکه بخواهیم بگیم کی بهتره! هرکسی هرچقدر هم عالی باشه توی سطح خودش باز چیزهای جدید برای فراگرفتن هست.
مقدمه
اگر شبکههای اجتماعی سیسوگ رو دنبال میکنید احتمالاً میدانید که چند هفتهای هست درگیر ساخت یه هوش مصنوعی شطرنج هستم که روی میکروکنترلر stm32 قابل اجراست و اولین قسمت این پروژه رو تحت عنوان “پیاده سازی هوش مصنوعی شطرنج” منتشر کردم. نکتهای که توی این پیاده سازی خیلی مهمه دریافت بهترین پرفورمنس از میکروکنترلر است. برای این که عملکرد قابل قبولی داشته باشه لازمه که یه سری بهینه سازیها روی کد انجام بشه نظیر این که مثلاً این که توابع پر استفاده به حافظه RAM منتقل بشن یا تا جای ممکن کد بهینه بشه. ایده این مسابقه هم دقیقاً از همینجا میاد. برای چالش چهارم با سیسوگ همراه باشید.
صورت مساله
با فرض این که از میکروکنترلر STM32Fxxx استفاده می کنیم سرعت اجرای حلقههای زیر به چه صورت است؟
1 2 3 4 5 6 | void loop_x() { volatile char i=200; while(i--) __NOP(); } |
یا
1 2 3 4 5 6 | void loop_y() { volatile int i=200; while(i--) __NOP(); } |
فکر میکنید کدام حلقه سریعتر اجرا میشود؟ یا شاید سرعت اجرای برابری دارند! شما چه فکر میکنید؟
شرایط داوری و جایزه
با توجه به صورت مسئله علاوه بر جواب لازم است دلیل آن نیز ذکر شود. پاسخهای صحیح با توضیحات کاملتر دارای اولویت بالاتری هستند. منظور از کامل بودن توضیحات صرفاً بلند بودن کامنت نیست بلکه دلیل باید به شکل گویا بیان شده باشد.
به دو نفر از کسانی که بتوانند جواب صحیح را با ذکر دلیل ارائه دهند به قید قرعه مبلغ دو میلیون ریال جایزه نقدی تعلق خواهد گرفت.
ارسال جواب
پاسخهای خود را در زیر همین پست کامنت کنید.
ممکن است که لازم باشد کدی را برای ما کامنت میکنید، در قسمت کامنت نظم کد به هم میریزد، بهتر است که ابتدا به سایت paste.ubuntu.com بروید، Syntax را زبان C انتخاب کنید و کد خود را در قسمت Content کپی کرده و بر روی Paste کلیک کنید و در نهایت فقط URL را در قسمت کامنت برای ما ارسال کنید.
مهلت پاسخ هم تا آخر روز شنبه 16 اسفند ماه ۱۳۹۹ است.
پایان و جواب چالش چهارم سیسوگ
پاسخ این مسابقه در ادامه هم به صورت ویدئو و هم به صورت متن وجود دارد.
اگر تمایل به دیدن ویدئو دارید، میتوانید پاسخ مسابقه را در ویدئوی زیر ببینید و اگر تمایل به خواندن متن دارید، متنی که پس از ویدئو قرار داده شده است، معادل با همین ویدئو است که میتوانید آن را بخوانید.
پاسخ متنی:
سوال مسابقه این بود که آیا نوع متغیر استفاده شده در حلقه، تاثیری در سرعت اجرای حلقه دارد یا خیر؟ برای همین شمارنده یک حلقه را از نوع char که یک بایتی است و حلقه دیگر را از نوع int که چهار بایتی است انتخاب کردیم.
و به عنوان یک کار مهم با استفاده از کلمه کلیدی volatile، کامپایلر را مجبور کردیم که متغیرها را درون RAM قرار بدهد. در واقع اگر ما از کلاس volatile استفاده نمیکردیم، ممکن بود کامپایلر متغیرها را درون رجیسترهای CPU قرار بدهد، که این موضوع مدنظر ما نبود.
خب برای تست اجازه بدهید برنامه را به صورت عملی بر روی یک میکروکنترلر اجرا کنیم.
برای این کار هر دو حلقه loop_x و loop_y را در main برنامه فراخوانی میکنیم، تا هر حلقه یک بار اجرا شود.
پس از این کار وارد محیط دیباگ برنامه میشویم تا کدهای اسمبلی معادل را بررسی کنیم.
خب میدانیم که قرار است متغیر i درون یک خانه از حافظه تعریف بشود، حال این خانه حافظه در کجا قرار دارد؟ این خانه حافظه در جایی قرار داد که SP به آن اشاره میکند. در ابتدا مقدار 200 را درون رجیستر R3 قرار میدهیم، سپس مقدار R3 را درون آن خانه از حافظه که گفتیم بارگذاری میکنیم. در واقع این عمل مقداردهی اولیه متغیر را انجام میدهد.
پس از آن وارد روتین حلقه میشویم، در روتین حلقه اولین کاری که نیاز است انجام بدهیم این است که مقدار متغیر را از حافظه بارگذاری و آن را درون رجیستر R3 قرار بدهیم.
پس از این مرحله، به مرحلهای خواهیم رسید که چالش اصلی را ایجاد میکند.
همانطور که میدانید میکروکنترلر ما 32 بیتی است و محاسباتی که ALU یک میکروکنترلر 32 بیتی میتواند انجام بدهد، حتما باید 32 بیت باشد. برای همیسن قبل از اینکه ما بخواهیم مقداری را از رجیستر R3 کم بکنیم، نیاز است که متغیر یک بایتی را به یک متغیر 32 بیتی تبدیل بکنیم.
از آنجایی که متغیر ما علامتدار است، در این مرحله از دستور اسمبلی sxtb استفاده میشود که این دستور یک متغیر 8 بیتی علامتدار را به یک متغیر 32 بیتی علامتدار تبدیل میکند.
پس از این مرحله، ALU یک واحد از متغیر کم میکند و این متغیر در رجیستر R2 ذخیره میشود. و در نهایت R2 را در خانه حافظه ذخیره میکنیم.
حال اجازه بدهید ببینیم وقتی متغیر شمارنده 32 بیتی است چه اتفاقی میافتد. در اینجا در روتین حلقه، پس از اینکه متغیر بارگذاری شد، بدون اینکه طول متغیر افزایش یا کاهش یابد، متغیر یک واحد کاهش مییابد و در دوباره درون حافظه نوشته میشود.
در واقی تبدیل 8 به 32 و 32 به 8 بیت، سیکلهای اضافی هستند که ما با تعریف متغیر یک بایتی به CPU تحمیل میکنیم. برای بهبود راندمان در اینجور مواقع ما میتوانیم شمارنده حلقه را 32 بیتی تعریف کنیم.
اما اگر ما متغیرها را درون RAM تعریف نکنیم چه اتفاقی میافتد؟
برای اینکه متغیرها درون RAM قرار نگیرند، باید کلاس volatile را از ابتدای تعریف متغیر یک بایتی و چهار بایتی حذف کنیم.
در این حالت متغیر دیگر گسترش پیدا نمیکند، چون دیگر قرار نیست داخل RAM نوشته بشود. یکی از رجیسترهای CPU به عنوان متغیر در نظر گرفته میشود و بر روی آن محاسبات ریاضی انجام میشود. اما باز متغیر 32 بیتی سرعت اجرای بالاتری دارد چرا؟
چون برای یک متغیر 8 بیتی نیاز است که بررسی کنیم آیا سرریز رخ داده است یا نه، اما کنترل سرریز در متغیر 32 بیتی نیاز به بررسی توسط نرمافزار ندارد و به صورت سختافزاری انجام میشود و نیاز به دستور اسمبلی ندارد.
پس توصیه میکنیم اگر میکروکنترلر شما 32 بیتی است، حتما از یک متغیر 32 بیتی برای شمارنده حلقه استفاده بکنید.
ببخشید انقدر تعداد نظرات من زیاده. میگم این کدهای اسمبلی بزرگتر بودن بهتر میبود.
یک پیشنهاد دارم. مطالبتون رو تحت CC BY منتشر کنید. یا حتی CC BY SA.
واقعا جالب بود. خیلی ممنون. من جدیدا یه stm32 خریدم هنوز فرصت بازی باهاش رو پیدا نکردم متاسفانه.
سلام اول ممنون از شما بابت مطرح کردن این نکته مهم ، اما خب باید عرض کنم جوابی که به این سوال دادید کاملا اشتباهه. به کد اسمبلی نگاه کنید متوجه میشوید.
در واقع بحث سرریز اصلا مطرح نیس
سلام دوست عزیز
ممکنه من اشتباه کرده باشم، خوشحال میشدم دلیلش رو از نظر خودتون بیان کنید
والا دلیلش نیاز به توضیحات طولانی داره ،اما به صورت خلاصه اگه برید توضیحات اون دستور اسمبلی را بخونید میبینید که داره Zero extending انجام میده که تئوری خودش را داره و اصلا ربطی به سرریز نداره
اجالاتا این توضیحاتی که دادید را اصلاح یا پاک کنید تا سر فرصت مناسب یه توضیح طولانی در موردش بنویسم و بدم که جایگزینش کنید
سلام متین عزیز
الان من دوباره مطلب رو مطالعه کردم، به نظر توضیحات مشکلی نداره البته قبول دارم که واژه سرریز احتمالا کژتابی داره و باعث سوءتفاهم میکننه بشه
منتظرم توضیحات تکمیلی شما هستم تا جایگزین کنم
متشکرم برای دقت شما
یعنی مقاومتتون در مقابل اینکه اشتباه کردید را دوست دارم
دوست من خب معلومه که با خوندن دوباره باز هم نمیفهمید که اشتباه کردید ، من گفتم اشتباه کردید که برید یکم معماری بخونید و این مورد را بررسی کنید نه اینکه دوباره نوشته خودتون را بخونید و بگید من بررسی کردم اشتباه نبود
سری اول اونجوری صحبت کردم که هر ادمی حق داره اشتباه کنه و شما هم دارید برا این نوشته ها زحمت میکشید پس بهتره با اون لحن بهتون بگم که اشتباه میکنید که پیش بقیه حالت بدی نداشته باشه ولی با خوندن یه مسابقه دیگه هم دیدم که اونجا هم یه اشتباه غاحش تر از این کردید و این به ذهن من این رو متبادر میکنه که شما فقط ادای حرفه ای بودن را در میاورید
بازم ببخشید من یکم تند صحبت کردم حقیقتا وقتی یه نفر اطلاعات غلط به خورد بقیه میده و قبول هم نمیکنه یکم کفری میشم و دوست ندارم بقیه با اطلاعات اشتباه کسی که خودش را در جایگاه معلم میدونه یه عمر یه موضوع را اشتباه یاد بگیرن
شاید بپرسید چرا خودم درستش را نمیگم حقیقتش اینه که من وقت این کار را ندارم فقط میتونم کیورد بگم و بقیه برن دنبالش و اگه دیدم اطلاعات غلطی داده میشه یاداوری کنم همین
موفق باشید
دوست عزیز ما اینجا سعی میکنیم مطلبی را پاک نکنیم و اگر قرار باشه تصحیح هم انجام بشه حتما ذکر خواهیم کرد و سعی میکنیم مطلب اشتباه را بگذاریم بمونه و دقیقا متذکر بشیم که حتی سابقه اشتباه ما هم بمونه
هر مسابقه ای برقرار شد دوستانی مثل شما بودند که گفتند پاسخ درست نیست یا فلان جا مشکل هست که برخی از اونها هم ادعاهای زیادی هم داشتند و البته مثل شما هم قضاوت کردند وانتظار دارند که سریعا ما مطلب را حذف کنیم و ویرایش و … ما نمیتونیم برای هر نوع ادعایی مطلب را حذف کنیم و یا ویرایش کنیم و در ضمن خودمون را هم عاری از خطا نمیدونیم
به جای بحث کردن و عصبانی شدن هم سعی میکنیم مطلب شما را عینا اینجا منعکس کنیم تمایل داشتید وقت بزارید یا نکاتی را عنوان کنید حتما اینحا قرار میگیره و اگر هم ما متوجه بشیم اشتیاه هست صراحتا اعلام میکنیم که همه بدونن, فرصت هم ندارید کیورد های خودتون را بدید ما هم منتشر میکنیم و خودمون هم اگر به جمع بندی شما رسیدیم حتما تصحیحات در پست انجام میشه
شما رو نمی فهمم، یه ادعایی میکنید و حاضر نیستید به جای دو سه پاراگرف حرف غیر مرتبط یک خط حرف فنی بزنید خوب باید در جوابتون چی بگن واقعا ؟
منم مدعی نیستم که آدمی هستم که سطح بالایی دارم و اتفاقا گفتم ممکنه اشتباه کرده باشم اگر بهم ثابت بشه اشتباه کردم اتفاقا گفتم مطلب رو ویرایش میکنم
بعد شما چکار کردید؟ فقط حرف زدید، و گفتید نه من میدونم شما اشتباه کردید،باشه مرسی که متوجه شدید من اشتباه کردم درستش رو هم زحمت بکشید بگید تا این کار رو نکنید میتونم استدلال کنم شما دارید اشتباه میکنید غیر اینه ؟
دوست عزیز این چیزی که شما بهش میگی کلید واژه، خودش اشتباهه 😐
در واقع همونطور که در توضیحات هم ذکر شده داره Sign extension اتفاق می افته نه Zero extension
مطالب خیلی عالی و تاثیر گذاربود.
سلام جناب زئوس
میبینم از خفا در امدید??
اتفاق خاصی افتاده قاعده رو عوض کردید??
خوب لااقل چشممون به جمالتون روشن شد☺☺
سلام دوست عزیز
البته ایشون خفا هم نبودند و قاعده خاصی هم نبوده:-)
این هم ساختار متفاوتی برای اموزش هست که البته دوستان زیادی اون را میپسندند سعی کردیم این روش را هم تست کنیم
بله بله صد البته
موفق باشید
حلقه loop_y سریعتر است، دلیلش هم به Zero extending برمیگردد.
اگر به کد اسمبلی این دو حلقه توجه کنیم، میبینیم که در حلقه اول دستور اسمبلی UXTB وجود دارد که در حلقه دوم وجود ندارد.
دستور اسمبلی UXTB در واقع عمل Zero extending یا همون اضافه کردن 0 به سمت چپ عدد را انجام میدهد (به دلیل مثبت بودن، عدد 0 اضافه میشود. اگر عدد ما منفی بود، عدد 1 اضافه میشد البته با یک دستور اسمبلی دیگر.)
حالا چرا این کار را انجام میدهد؟ چون نوع char با طول 8 بیت است، برای انجام عملیات ریاضی two’s complement که مرجع آن بالاتر از 8 بیت، خواه 16 بیت یا 32 بیت باشد در واقع این یک اجبار است و حتما باید عمل Zero extending انجام بشود.
اما در نهایت دلیل اصلی این که چرا این کار در ARM انجام میشود برمیگردد به تئوری two’s complement که بحثش کمی مفصل است. اما به طور خلاصه برای انجام عملیات ریاضی در این سیستم اعداد باید طول اعداد از قبل مشخص باشد و اگر طول عددی کمتر از مرجع در نظر گرفته شده است حتما باید با توجه به علامتش اکستند شود.
خب میشه خیلی بیشتر در این مورد توضیح داد. این توضیحات شاید بنا به کوتاه بودن کمی گنگ باشند، اما سعی کردم خلاصه و مفید دلیلش رو بگم.
دقیقا این اتفاق بخاطر ۳۲ بیت بودن پردازنده می افته و جواب شما درسته
با توجه به نوع میکروکنترلر گفته شده که 32 بیتی است ، سرعت اجرای دومین حلقه که با int است بیشتر است. زیرا سخت افزار به گونه ای طراحی شده که برای داده های ۳۲ بیتی کارایی بهتری دارد.
متغیر int ،
32 بیت و متغیر char ،
8 بیتی است … cpu ممکنه ممکن است دستور العمل هایی جهت load , store و opration روی داده های ۳۲ بیتی داشته باشد و چنین دستورالعمل هایی برای داده از نوع char وجود نداشته باشد ، حال برای بارگزاری یک مقدار 8 بیتی ، باید یک مقدار 32 بیتی را بارگزاری کند پسمجبور است این 8 بیت را extract کند که خود زمان میبرد …
همچنین برای ذخیره char که 8 بیتی است ممکن است مجبور باشد ۳۲ بیت را بارگزاری کند ، پس برای تنظیم زیرمجموعه ۸ بیتی مورد نظر باید از عملیات bitwise استفاده کند و سپس آن را ذخیره کند
بله کاملا درست است.
سرعت لوپی که کانتر اون int هستش بیشتره، دلیلش هم اینه که دستورالعمل هایی برای عملیات ها وجود داره که به صورت ۳۲ بیتی کار میکنن، حالا ما از داده ۸ بیتی استفاده کنیم باید تبدیل داده انجام بدیدم که زمان میبره
بله همینطوره 🙂
ببخشید عجله کردم در ارسال دیدگاه
دلیلش رو کامل توضیح ندادم
یکی از دلایلی که دارم ساختار ALU هست وقتی واحد محاسبات منطقی هشت بیتی باشه برای یک داده 16 بیتی نیاز به دوبار پردازش اون داره ولی زمانی که این واحد 16 یا 32 بیتی باشه مسلما فرقی نمیکنه داده 16 32 یا 8 بیتی هست.
خیلی دوست داشتم شرایط تستش رو سخت افزاری داشتم تا این تئوری رو ازمایش کنم
به نظر اگر در یک شبیه ساز مانند پروتیوس میشد یک لایبرری قابل شبیه سازی پیدا کردبرای STM32 حتما یک بنچمارک میگرفتم بعد جواب میدادم با اینحا 3 ماه دیگه که خدمتم تموم شد حتما یه سر به این URL میزنم خیلی مشتاق پاسخ درست هستم…
در سخن پایانی باید بگم وجود یک واحد محاسبه گر منطقی 32 بیتی فقط یکی از دلایل پردازش سریعتر حافظه میتونه باشه! برای اینکه پاسخ سریح تری بدم به وقت بیشتری برای مطالعه دیتاشیت یک میکروکنترلر پیچیده مانند ARM با اینهمه پیشرفت اونم برای اولین بار نیاز دارم
پاینده و پیروز باشید زنده باد Open sorce
برای انجام محاسبات باید عدد توی یکی از رجیسترهای پردازنده لود بشه – و تمام رجیسترها ۳۲ بیت هستند- پس نیازه که برای لود یک عدد یک بایتی اونو کست کرد به ۳۲ بیت که همین کار زمان بیشتری رو در پردازنده میگیره.
ببخشید عجله کردم در ارسال دیدگاه
با سلام و خسته نباشید
بنده تازه از AVR به Stm32 مهاجرت کردم (مهاجرت کلمه مناسبی نیست هردو میکروکنترلر عالی هستند) در risc های 8 بیتی داده نوع هشت بیتی زودتر پردازش میشه اما در مورد STM32 به نظرم با اینکه هنوز برگه اطلاعاتی اون رو مطالعه نکردم بخاطر معماریش زمان اجرای دو حلقه برابر هست یقین دارم
سلام دوست عزیز
در واقع به دلیل این که ثبات ها و رجیسترهای میکروکنترلر ۳۲ بیتی هست باید عدد خوانده شده از روی حافظه به یک عدد ۳۲ بیتی تبدیل بشه تا محاسبات ریاضی روی اون انجام بشه و همین امر زمان بیشتری خواهد برد.
در واقع حلقه دوم سریعتر اجرا میشه چون از همون ابتدا به شکل ۳۲ بیتی است و این عمل ۳۲ بیتی کردن داده نیاز نیست روی اون انجام بشه.
عرض سلام و ادب
حلقه دوم سریع تر است.
با توجه به اینکه در stm32 باس حافظه و حافظه 32 بیتی است، در حلقه دوم int 32 بیتی است و بدون هیچ عملیات اضافه ای به آن دسترسی پیدا می کند اما در حلقه اول char یک بایت است و برای دسترسی به آن ابتدا باید تبدیل به 32 بیت شود تا در باس حافظه که 32 بیتی است قرار گیرد و در دسترس CPU قرار گیرد. بنابراین چند عملیات اضافه انجام می دهد.
بله کاملا درسته 🙂
سرعت اجرا حلقه با متغیر اینتجر سریعتر است. متغیر اینتجر در کامپایلر های معمول اس تی ام ۳۲ به صورت یک عدد ۳۲ بیتی با علامت دیده میشه، با توجه به معماری کورتکس ام ۳ که از دستورات فشرده ۱۶ بیتی استفاده میکنند (بر خلاف نسل های قبلی مثل آرم ۷ ها که هم دستورات قوی ۳۲ بیتی داشتند و هم دستورات فشرده ۱۶ بیتی) محاسبه های ریاضی و منطقی فقط به صورت ۳۲ بیتی انجام میشود. در نتیجه عملیات های ۱۶ بیتی و ۸ بیتی(متغیر کاراکتر) با چند محاسبه و مقایسه به صورت ۳۲ بیتی انجام میشود که سرعت اجرا حلقه را کندتر میکند . برای اطمینان از این عملکرد میکروکنترلر میتوان سرعت اجرا برنامه را با یک تایمر یا حتی سیستیک تایمر هم استفاده کرد.
بله درسته همینطوره
متغییر نوع char نمیتونه عدد ۲۰۰ رو ذخیره کنه و بجاش عدد منفی ۶۳ جاش میشینه. بعد اون حلقه char حدود ۶۴ بار اجرا میشه که به مراتب کمتر از حلقه int هست و این حلقه سریعتر اجرا میشه
متغییر char به شکل پیش فرض بدون علامت در نظر گرفته میشه !
نه عزیز. در تمام پلتفرم ها پیشفرض signd هست. از زمان z80 تا امروز صبح !!!!!
اوه – بله حق با شماست، توی ادیتوری که من استفاده میکنم به شکل پیش فرض تیک بی علامت بودن خورده
با این حال شک نکنید حلقه ۲۰۰ بار اجرا خواهد شد – چرا ؟
شما وقتی از منفی ۵۷ یکی کم کنید میشود منفی ۵۸ و این تا منفی ۱۲۸ ادامه خواهد داشت بعد به مثبت ۱۲۷ خواهید رسید و تا صفر ادامه خواهد داشت.
پس حلقه شما همچنان ۲۰۰ بار اجرا خواهد شد.
کاری که اغلب دوستان انجام دادند 🙂
در واقع نه. signed یا unsigned بودن char بسته به پیادهسازی و معماری داره:
استاندارد C99:
https://i.imgur.com/NlP7fWK.png
https://stackoverflow.com/a/40772217/1506761
ابتدا باید توجه کرد که برای مقایسه معنادار باید بهینهسازی کامپایلر خاموش شود (سوییچ O0). volatile کردن متغیرها هم به این موضوع کمک کرده است.
تفاوت دو روش مورد اشاره این است که 200 در واقع یک int هست. هنگامی که این مقدار به پارامتر حلقه cast میشود، از آنجایی که نوع پارامتر i از نوع char هست، مقداری که در واقع درون i وجود دارد اصطلاحاً implementation defined است.
در معماری Arm Cortex M، در حالت char به جای اینکه مقدار ۲۰۰ که قرار بود در یک رجیستر ۳۲ بیتی ذخیره شود،مقدار منفی ۵۵ در یک بایت قرار گرفته است. این باعث میشود که پروسسور برای جابه جایی بایت ها، از دو instruction اضافی استفاده نماید. در نتیجه loop باید زمان بیشتری را برای صفر کردن این عدد صرف کند.
در تصویر زیر، تفاوت disassembly این دو روش (char و int) نشان داده شده است: (خطوط .loc مربوط به دیباگ هستند و حذف شده اند)
https://i.imgur.com/VcvpnwN.png
این کد در STM32F103 تست شد (صدهزار مرتبه) :
تفاوت زمانی به شکل زیر است:
tests: 100000, total char_version(): 3407 millisecs.
tests: 100000, total int_version(): 2826 millisecs.
بله درسته در واقع نیازه که متغششر قبل از بارگیری کست بشه.
سلام، اول اینکه باید به بررسی معماری و عرض بیت هسته ها پرداخته بشه، عرض بیت هسته و رجیستر های CortexM دارای 32 بیت هست، این به این معنی هست که وقتی متغیری تعریف میکنیم مقدارش درون رجیستر های 32 بیتی پردازش میشه، حالا میریم سراغ متغیر های تعریف شده:
1. متغیر اول از نوع int هست و به صورت پیشفرض 32 بیتی به حساب میاد و طولش به اندازه طول رجیستر ها است.
2. متغیر دوم از نوع char است و طول آن برابر با 8 بیت است.
تا اینجا متوجه میشیم که متغیر دوم کل بیت های رجیستر را استفاده نمیکند، پس پردازش روی این متغیر به چه صورت هست؟
در Cortex-M وقتی پردازش روی متغیر هایی کوچکتر از سایز رجیستر هاش انجام میشه کامپایلر بیت های اضافی را با 0 پر میکند! (جهت تبدیل به سایز بزرگتر)
بررسی کد:
تا قبل از حلقه while میزان پردازش را میتوان برابر در نظر گرفت (با توجه به اینکه دستورات پردازنده برای هردو وجود دارد)، اما وقتی وارد حلقه میشویم روی متغیر پردازش انجام میدهیم، در کد اول با متغیر int نیازی به پر کردن بیت های دیگر نیست ولی در کد دوم بدلیل استفاده از char پس از بارگزاری داده در رجیستر و بعد از کاهش مقدار متغیر نیاز به پر کردن بیت های 31~8 با مقدار 0 داریم، در نتیجه دو دستور دیگر نیز به ساختار کد اضافه میشود.
در نتیجه تابع loop_y سرعت بیشتری در اجرا دارد!
بله کاملا درسته 🙂
حلقه دوم صحیح است
در arm ثبات ها و پشته و تمام ورودی های آن اکثر دستورهای پردازش داده arm ۳۲ بیتی هستند.به همین دلیل بایستی درصورت امکان از انواع داده ۳۲ بیتی مثل int برای متغیر های محلی استفاده کنیم (این تفکر اشتباه است چون char فضای کمتر اشغال میکند بهتر است از آن استفاده کنیم….)
بله درسته – در اوقع باید ببینیم معماری سخت افزار چی هست و بر اساس اون معماری انتخاب انجام بدیم.
سلام
چرا جواب من پاک شده؟
:-((((
اوپس متاسفم !
ابتدا فکر کردم شاید 32-bit بودن باعث شود که فرقی بین 2 کد نباشد. بنابراین گفتم از این مسئله مطمئن شوم و جواب را ارسال کنم. با استفاده از DMA به منظورتغییر وضعیت یه پین با هربار تکرار شدن حلقه، متوجه تفاوت ناچیز فرکانسی 2 حلقه شدم. حلقه ای که از int استفاده میکرد کمی سریعتر بود و تقریبا 3% فرکانس بالاتری داشت.
برای بررسی بیشتر از تبدیل کد c به اسمبلی استفاده کردم و متوجه شدم کاهش یک واحد توی 4 بایت (DWORD) یک مرحله کوتاه تر از یه تک بایته.
برای بررسی صحت این قضیه، کد زیر رو توی https://godbolt.org/ با هر کامپایلر c به اسمبلی موجود بررسی کنید.
void loop_x()
{
volatile char i=200;
while(i–)
__NOP();
}
void loop_y()
{
volatile int i=200;
while(i–)
__NOP();
}
بله درسته حلقه int سریعتر است.
سلام با توجه به استفاده از تابع اصلاح کننده volatile سرعت مقدار دهی متغییر به دلیل نیاز نبودن به درخواست ها و حلقه ها بیشتر میشود هرچند که در اینجا بی تاثیر هست چون داخل یک حلقه فقط برنامه اجرا شده و دوم اینک در هر دو مورد سوال از این تابع استفاده شده
مثلا بعدی متغیر char هست که ۸ بیت فضا دارد در حالی که متغیر int فقط ۲ بیت فضا دارد و مقدار دهی int به دلیل کم بودن فضا بیشتر است
به نظر من مورد دوم سوال که با int انجام شده سرعت بیشتری دارد
بله حلقه ای که با int است سریعتر است
و این به دلیل volatile بودن متغیر هاست که کامپایلر را مجبور میکند متغییرها رو داخل رم ذخیره کند.
سرعت اجرای تابع loop_x کمتر از تابع loop_y است . ولی حلقه های تعریف شده در هر کدام با سرعتی یکسان اجرا میشوند .
من فکر میکنم سرعت اجرای حلقه ها هم متفاوت خواهد بود چون متغییرها درون رم تعریف میشوند
در اینجا ما در هر فراخوانی تابع یک ارجاع به آدرس دیگر داریم . به آدرس حلقه . و این ربطی به بیرون حلقه نداره . int یا char بودن متغیر در حلقه تاثیر گذار نیست . چرا که خود char یک نوع int . باید توجه داشته باشیم که حلقه پس از فراخوانی تابع ، فراخوانی میشه .
دوست عزیز تاثیر گذار هست – اجازه بدید جواب تفصیلی رو منتشر کنیم – خواهید دید
کدی که با int نوشته شده سریعتر خواهد بود
چونکه کامپایلر نیازی به تبدیلش نداره تا عملیت ریاضی رو انجام بده
حتی توی کامپیوتر هم همینه
دلیل وجودی bool هم همینه
بله درسته 🙂
درود
باهم فرقی ندارن از لحاظ سرعت اجرا
معماری آرم ورژن ۷ طبق مستنداتش برای خوندن و نوشتن مقادیر ۸ بیتی ۱۶ بیتی و ۳۲ بیتی میتونه از دستورالعمل های اتومیک که یک سیکل ماشین زمان میخوان استفاده کنه ،تعریف متغیر شما و مقدار دهی بهش یک سیکل زمان میگیره
ولی احتمال میدم اگه متغیر رو ۳۲ بیتی تعریف میکردین
حلقه وایل متفاوت تر ترجمه میشد و سرعت اجرا بیشتر میشد.
کد رو تست نکردم دسترسی ندارم
?
در واقع مساله اینه که حلقه int سریعتر اجرا میشه
بخاطر این که متغیرها توی رم هستند و هنگام لودشدن توی رجیسترهای cpu نیازه که به سی و دو بیت کست بشه و همین زمان بر خواهد بود.
من فکر میکنم برای میکروکنترلر stm32 فرقی نداره سرعتشون تو اجرای این حلقه ها.
چون نوع int دو بایت هست میشه ۱۶ بیت ، و میکرو میتونه ۳۲ بیت رو تو یه کلاک پالس بنویسه ، حالا چه فرقی داره داده char یه بایتی یا ۸ بیتی باشه یا int دو بایتی یا ۱۶ بیتی.
خوب توی میکروکنترلرهای ۳۲ متغییر int به شکل ۳۲ بیتی تعریف میشه
مساله دقیقا برمیگرده به لود کردن مقادیر از توی رم که برای int نیاز به کست کردن نداره ولی برای char نیازه این اتفاق بیفته.
با سلام .
من فکر میکنم اون کدی که متغیر رو به صورت int تعریف میکنه سریعتر هست .
stm32 ها 32 بیتی هستن .
من در حالت دیبا گ کدهای اسمبلی رو در یک مورد نگاه میکردم .
وقتی متغیر رو 8 بیتی تعریف میکردم برای دسترسی به اون دستورات اسمبلی بیشتری استفاده میشد .
ولی برای دسترسی به متغیر 32 بیتی فرمانهای کمتری استفاده شده بود. فکر میکنم به خاطر اینکه باس دیتای cpu طولش 32 بیت هست این اتفاق میفته .البته کمپایلری هم که من استفاده کردم GCC بود.
به نظر میرسه برای stm32 دسترسی 8 بیتی به صورت مستقیم امکانپذیر نباشه و علت این باشه.
بله درسته کاملا 🙂
با توجه به ۳۲ بیت بودن این سری میکرو ها، استفاده از متغییر int در تبدیل به دستورات اسمبلی به کدهای کمتری تبدیل می شود همچین به معماری حافظه این میکرو سازگارتر، بطوریکه مدیریت این کدها کمتر و سریع خواهد بود.
لذا حالت دوم سریعتر هست.
بله کاملا درسته 🙂
درکل ۳۲ بیت در stmما هست ک یک ریجستر کامل رو تشکیل بده که ب نامgeneral purposeهست و ثبات هایی ب نام R0,R1,..در ان ها وجود داره هر R ۵بیت هست در حالتchar ۲ تا از Rها درگیر میشن و در حالت کلی برای ریجستر *۳(۲×۵) که میشه ۳۰ بیت پس ۷تا باید R وجود داشته باشد
چون تو حالت char که 8بیتیه باید 7 تا rداشته باشیم ولی تو int که ۱۶ بیتی هست و ۵*۳ و ۳تا ثبات rنیازه ولی در ۳۲ بیت ۶ تا ثبات r نیاز هست
6تا rداریم پس در هر صورت intسرعت بیشتری داره
چطور به سه تا ثبات برای نگه داری یه عدد ۱۶ بیتی رسیدید ؟
در ضمن متغییر int در خانواده های ۳۲ بیتی به شکل ۳۲ بیتی تعریف میشه
چون تو حالت char که 8بیتیه باید 4 تا rداشته باشیم در قسمت ریجیستر (general purpose )
ولی تو int که ۱۶بیتی هست
3تا rداریم
پس intسرعت بیشتری داره
ببینید این که حلقه int سریعتره درسته
ولی توضیح شما رو متوجه نشدم :/
قطعا این کد اصلا اجرا نمیشه چون توی حلقه وایل هیچ شرطی نوشته نشده
😐
چرا اجرا نشه دوست عزیز – همین که i مقداری غیر صفر داشته باشه حلقه صحیح است و تا وقتی که i مقداری غیر صفر داشته باشه اجرا میشه :/
سلام . توی یه پست اموزشی داخل سایت در مورد بهینه سازی کد توی میکروهای avr گفته بودید که استفاده از متغیر هشت بیتی برای این پردازنده های هشت بیتی کارامد تر هستش نسبت به استفاده از متغییر 16 بیتی . پس اینجا هم چون میکروی مورد نظرمون 32 بیتی هست ، اسفاده از int کار امد تر هست. انشاا…
بله منطق و جواب درستی است 🙂
از مقایسه ی اینستراکشن های دوتابع بصورت disassemble شده، میتوان دید که در حلقه ی while موجود در تابع loop_x برای لود کردن متغیر i از حافظه، کم کردن یک واحد از آن و ذخیره ی متغیر درحافظه، علاوه بر دستورات ldrb، strb، cmp که متناظر این دستورات در حلقه ی while موجود در تابع loop_y نیز مشاهده میشود، شاهد دو دستور and هستیم. از انجایی که این حلقه 201 بار اجرا میشود، در تابع loop_x هزینه ی اجرای 402 (201 * 2) دستور and وجود دارد که این هزینه در تابع loop_y وجود ندارد. در نتیجه میتوان گفت که تابع loop_y سریع تر از تابع loop_x اجرا خواهد شد.
لینک اینستراکشن توابع:
https://paste.ubuntu.com/p/2JxszDMgxn
برای کامپایل و disassemble کردن این توابع بصورت زیر عمل کردم:
اول برنامه ای نوشتم که در اون فقط دو تابع رو پیاده سازی کردم و تابع main رو صدا زدم(نیازی به فراخوانی توابع در تابع miain نبود). سپس با کامند زیر برنامه رو کامپایل کردم.
( arm-none-eabi-gcc –specs=nosys.specs loop.c -o loop.out )
سپس توسط دو دستور زیر اینستراکشن های توابع رو استخراج کردم و در دو فایل جدا از هم ذخیره کردم.و محتویات دو فایل رو کنار هم ارسال کردم.
(arm-none-eabi-objdump –no-show-raw-insn –visualize-jumps –disassemble=loop_x ./loop.out > loopx.objdump )
(arm-none-eabi-objdump –no-show-raw-insn –visualize-jumps –disassemble=loop_y ./loop.out > loopy.objdump )
بله درست است 🙂
این نیاز به تست روی معماری همون میکرو داره و همچنین وابسته به کامپایلر هستش ولی با فرض اینکه با -O0 کامپایل میکنید zero extension میتونه overhead داشته باشه و لوپ با uint_8 کندتر باشه
بله درسته
حلقه دوم سریع تر است
شاید به این فکر کرده باشید که اگر این کد را در یک سیستم 8 بیتی مانند avr تست کنیم ، کدام حلقه سریع تر است؟ بله حلقه 1 قطعا در این سیستم سریع تر است چون یک تفریق 32 بیتی در یک سیستم 8 بیتی به چند کلاک متوالی نیاز دارد و این کار باعث کند شدن سیستم می شود
اما در یک سیستم 32 بیتی چی طور ؟ شاید بگید چون سیستم 32 بیتی هست ، پس یک تفریق 32 بیتی را می تونه تو یک کلاک انجام بده . بدین ترتیب پس یک عملیات 8 بیتی رو هم باید در یک کلاک انجام بده . بله جواب درسته . پس چرا دو حلقه بالا باهم متفاوتند ؟
جواب به قبل تفریق بر می گرده وقتی می خواد پردازنده دیتا رو از رم به ریجستر ها خودش منتقل کنه. می دونید که در stm32 رجیستر های cpu طولشون 32 بیت هستش . پس وقتی یک داده 32 بیتی رو از رم به یکی از رجیستر های cpu منتقل می کنیم در واقع 32 بیت داده رم ، رو 32 بیت داده رجیستر ریخته میشوند. اما اگه دیتا 8 بیتی داشتیم چی ؟ خوب اون دیتا می یاد رو 8 بیت اول ریجستر cpu ریخته میشه . اما تکلیف 24 بیت بعدی چی میشه ؟ آیا تضمینی وجود داره که اونا صفر باشن ( مشکل ایجاد نکنن ) جواب خیر هستش در واقع کد باید کاری کنه که وقتی یک دیتا 8 بیتی از رم به cpu منتقل میشه یک فکری به حال 24 بیت بعدی بکنه اگر دیتا بی علامت باشد باید عملیات zero extention انجام بده یعنی 24 بیته بعدی رو صفر بکنه اما اگر عدد علامت دار باشه باید signed extention بکنه یعنی مثلا عدد 11110001 که برابر -15 هستش به 32 بیت گسترش بده که مقدار در 32 بیت همون -15 بشه ( 0xfffffff1) . این عملیات ها هستن که باعث میشه پردازش یک داده 8 بیتی در سیستم 32 بیتی با سرعت کمتری به نسبت داده 32 بیتی انجام بشه
در ادامه با تست عملی مشخص شد که حلقه دوم در cortex m3 و با فعال بودن بهینه سازی رو سایز ، حدود 2017 کلاک و حلقه اول با حدود 402 کلاک بیشتر اجرا می شوند.
چون حلقه 201 بار اجرا می شود و اختلاف 402 تایی کلاک دو حلقه = دو دستور extetion برای گسترش دادن دیتای 8بیتی توسط کامپایلر ایجاد شده است
خروجی اسمبلی کامپایلر gcc را برای cortex m3
https://paste.ubuntu.com/p/zX9hfxQHNG/
بله درسته – یکی از کامل ترین جواب هایی که دیدم 🙂
با عرض سلام خدمت گروه بسیار فعال و ارزشمند سیسوگ.
من این دو حلقه رو در دیباگر و محیط دیس اسمبلر مشاهده کردم و طبق مطالعه ای که روی دستورات instruction set Cortex انجام دادم
به این نتیجه رسیدم که loop_y که متغیر آن از نوع int تعریف شده دارای سرعت بیشتریه.
چون کامپایلر برای تفسیر کدی که از متغیر char استفاده شده میاد از دستورات اسمبلی مربوط به byte استفاده میکنه مثل دسترور STRB و UXTB
و … تا بتونه با مقادیر به صورت byte کار بکنه در صورتی که تابعی که با متغیر از نوع int تعریف شده چون سایز متغیر 32 بیتیه و چون آدرس دهی در پردازنده ی ما هم 32 بیتی هست خیلی راحت و با دستورات کمتر کامپایلر میتونه کد رو به اسمبلی ترجمه کنه (همچنین حجم کد اسمبلی while در تابعی که از متغیر int استفاده شده بود کمتر از تابعی بود که از متغیر char استفاده شده )
با تشکر.
بله کاملا درسته 🙂
تابع اولی سریعتر باید باشه چون Char یک بایتیه ولی int دو بایتی…
البته توی میکروی ۳۲ بیتی اینت هم ۳۲ بیت خواهد بود
و چون رجیسترها و ثبات ها ۳۲ بیتی هستند در واقع حلقه int سریعتر اجرا میشه
سلام
من فکر میکنم حلقه ی دوم جواب درست باشه
چون که متغیر از نوع int برای این منظور مناسب تره
چون متغیر از نوع char حجم بیشتری از رم رو اشغال میکنه در صورتی که نیازی نیست و متغیر از نوع int برای این حلقه کفایت میکنه
این که حلقه دوم سریعتر اجرا میشه درسته ولی متغییر char رم کمتری میگره ها !
سلام
با توجه به اینکه هسته از معماری ۳۲ بیت استفاده میکنه و متغیر ها ۳۲ بیتی آدرس دهی میشن، قاعدتا برای تعریف نوع int یا char چهار بایت در نظر گرفته میشه و با توجه به اینکه برای اجرای دستور x– یا y– یه کلاک صرف خواهد شد، سرعت اجرای هر دو تابع برابر خواهد بود.
ارادتمند.
سلام دوست عزیز
نکته که ای که باید بهش توجه میکردید این هست که متغییرها قرار از توی رم لود بشن و ذخیره بشن
همین نکته کوچک معادلات رو بهم میریزه 🙂
متغیر char هشت بیت و متغیر int شانزده بیت است پس فضای کمتری اشغال میکنه و نصف اونه، اما اگر از میکرو ۳۲ بیتی استفاده کنید فرقی نداره چون در هر کلاک میتونه تا ۳۲ بیت رو پردازش یا منتقل کنه اما اگر از میکرو ۸ بیتی استفاده میکردید تفاوت در سرعت وجود داشت و استفاده از char سریعتر بود. زیرا برای خواندن int دوبار مراجعه به ram نیاز بود.
در میکروکنترلرهای ۳۲ بیتی متغییر اینت نیز ۳۲ بیت خواهند بود
درسته که عملیات ریاضی مورد نظر بر روی هر کدام از این متغییرها یک سیکل نیاز داره ولی بارگذاری اونها توی رجیستر پردازنده متفاوت خواهد بود.
سلام
جواب سوال حلقه x هست، به دلیل استفاده از char که یک بایتی هستش.
اما حلقه y سریعتر اجرا میشه 🙂
دقیقا به خاطر معماری ۳۲ بیتی پردازنده و ۳۲ بیتی بودن متغییر ذخیره شده !
من فکر کنم میکرو حلقه ای رو که متغیرش intهستش رو سریعتر تموم میکنه … بخاطر اینکه نیازی نیست متغیر رو cast کنه …بخاطر ۳۲ بیتی بودن میکرو … چون توابع رو هم اگر ورودی و خروجی تابع int باشه سریعتر اجرا میکنه ( فکر کنم از نوشته های خود شما بود ، مطمئن نیستم ) … امید وارم اشتبا نکرده باشم … ممنون … به امید دیدار ….
بله درسته 🙂
حلقه دوم سریع تر است
بله درسته ای کاش دلایل خودتون رو ارائه می کردید.
حلقه دوم
سلام
من دقیقا از معماری و دستورات اسمبلی STM32 خبر ندارم ولی به صورت کلی به نظرم کد دوم (loop_y) که متغیر از نوع int داره سریع تر هستش. دلیلش هم اینه که STM32 همونطور که از اسمش مشخصه باس 32 بیتی داره و نوع int هم 32 بیت پهنا داره. پس با کمینه کلاک میتونه به متغیر int دسترسی داشته باشه و روی اون عملیات انجام بده. اما در عوض نوع char پهنای 8 بیتی داره و لازمه علاوه بر این که به این متغیر دسترسی پیدا میکنه، محتویات حافظه مجاور رو هم تغییر نده که این خودش احتیاج به پردازش و کلاک بیشتر داره.
بله درسته از منطق خوبی استفاده کردید 🙂
من با میکروکنترلر و کامپایلرش آشنایی ندارم ولی یک حالت اینه که char یک بایت هست پس عملیات کمکردنش باید سبکتر باشه و سریعتر. حالت دیگه که احتمال بیشتری میدم، اپتیمایز کردن کامپایلر هست که با دیدن nop یا در حقیقت ندیدن هیچ عملیات داخل حلقه، کل اون را با ست کردن i به 0 جایگزین میکنه و در عمل دو تا حلقه یک پرفورمنس خواهند داشت.
از اونجایی که عملیات باید به شکلی باشه که روی خونه های دیگر حافظه تغییری ایجاد نکنه توی نوع char پس باید کار بیشتری رو انجام بده منطقا و حلقه char کندتر خواهد بود.
با توجه به اینکه معماری 32 بیتی هست loop_y باید سریع تر باشه . اگر کامپایلر int رو 32 بیتی بگیره و معماری هم 32 بیتی باشه سریعترین ادرسدهی هست
بله منطق شما درسته
با int سریع تر است چون میکرو stm32 پردازنده ۳۲ بیتی دارد و محاسبات در همان نوع سریع تر از ۸ بیتی است
جوابتون درسته ولی نه به این دلیل که محاسبات نوع ۳۲ بیتی سریعتره به دلیل که دسترسی به حافظه ۳۲ بیتی سریعتره