حسابداری

حسابداری

تمرین یادگیری

مقدمه

thunk

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

در واقع، می‌توان با توابع هم مانند هر آدرس حافظه‌ی دیگری رفتار کرد؛ یعنی می‌توان آن‌ها را در ثبات‌ها بار کرد، دست‌به‌دست کرد و در حافظه ذخیره کرد. همچنین می‌توان با call یا jmp جریان اجرا را به تابعی منتقل کرد که در یک ثبات یا در حافظه ذخیره شده است:

section .text
sum_op:
    lea rax, [rdi + rsi] ; loads the sum rdi + rsi into rax
    ret

apply_sum:
    lea rax, [rel sum_op]
    jmp rax   ; tail call

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

کد به‌عنوان داده

آدرس توابع را نیز می‌توان در حافظه ذخیره کرد و بعداً بازیابی کرد:

section .bss
    cached_fn resq 1

section .text
save_op:
    mov qword [rel cached_fn], rdi
    ret

apply_op:
    ; arguments are already set up according to the ABI
    jmp qword [rel cached_fn] ; tail call

save_op آدرس تابعی را که می‌گیرد در cached_fn می‌نویسد. این مقدار پس از بازگشتی که save_op می‌کند باقی می‌ماند، پس هر فراخوانی بعدیِ apply_op با یک پرش پایانی به آخرین آدرس ذخیره‌شده می‌رود. این کار امکان می‌دهد که هنگام اجرا تغییر دهید apply_op کدام تابع را فراخوانی می‌کند.

جدول ارسال

ذخیره‌ی آدرس توابع در یک آرایه این امکان را می‌دهد که بر پایه‌ی یک اندیس، که ممکن است به شرطی در زمان اجرا وابسته باشد، توابع مختلفی را انتخاب کنید. به این کار «جدول ارسال» می‌گویند:

section .data
    dispatch_table dq add_op, sub_op, mul_op

section .text
dispatch:
    ; this function takes two arguments in rdi and rsi, and an index in rdx
    ; it then applies the function corresponding to the index in rdx to the arguments
    lea rax, [rel dispatch_table]
    jmp qword [rax + 8*rdx]   ; tail-call the function address for the index

thunk حالت‌دار

thunk‌ای که بین فراخوانی‌ها حافظه‌ای ماندگار را می‌خواند یا به‌روزرسانی می‌کند، بسته به آنچه پیش‌تر رخ داده است ممکن است رفتار متفاوتی داشته باشد. ممکن است نتیجه‌ی آن به چیزی بیش از تنها آرگومان‌هایش بستگی داشته باشد.

برای نمونه، یک شمارنده که تابعی را می‌گیرد و آن را با شمارش جاری فراخوانی می‌کند و هر بار شمارش را یک واحد جلو می‌برد:

section .data
    count dq 0

section .text
tick:
    mov rax, rdi               ; saves the function address
    mov rdi, [rel count]       ; loads the current count as the function's argument
    inc qword [rel count]      ; advances the count
    jmp rax                    ; tail-calls the function

tick تابع داده‌شده را با شمارش جاری به‌عنوان آرگومانش فراخوانی می‌کند و سپس شمارش را جلو می‌برد. بنابراین نخستین فراخوانی tick(square) تابع square(0) را فراخوانی می‌کند، فراخوانی بعدیِ tick(square) تابع square(1) را، فراخوانی بعد از آن square(2) را و به همین ترتیب.

نمونه‌ی دیگری محاسبه‌ی تأخیری است:

section .bss
    captured_fn resq 1
    argument resq 1

section .text
delay:
    mov qword [rel captured_fn], rdi ; saves the function
    mov qword [rel argument], rsi    ; saves the argument
    lea rax, [rel invoke]            ; returns the `invoke` function
    ret

invoke:
    mov rdi, qword [rel argument]    ; loads the saved argument into `rdi`
    jmp qword [rel captured_fn]      ; tail-calls the saved function

delay یک تابع و یک مقدار می‌گیرد، آن‌ها را ذخیره می‌کند و invoke را برمی‌گرداند. وقتی invoke فراخوانی شود، تابع گرفته‌شده را با آرگومان ذخیره‌شده اجرا می‌کند.

بسیاری از الگوهای رایج در زبان‌های سطح بالاتر، مانند callback‌ها، متدهای مجازی، generator‌ها، currying، ترکیب توابع و بسیاری موارد دیگر، بر پایه‌ی thunk‌هایی ساخته شده‌اند که با حالت ماندگار جفت شده‌اند.

دستورالعمل‌ها

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

چهار وظیفه دارید.

Note

می‌توانید فرض کنید که هر «thunk» (تراکنش‌ها و نگهبان‌ها) در این تمرین تابعی است که:

  1. یک عدد صحیح نامنفی ۶۴بیتی را به عنوان آرگومان می‌گیرد
  2. و یک عدد صحیح نامنفی ۶۴بیتی هم برمی‌گرداند.

1. یک تراکنش را به خاطر بسپارید

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

دو تابع تعریف کنید:

  • remember_transaction یک تراکنش می‌گیرد و آن را در حافظه ذخیره می‌کند.
  • apply_remembered یک موجودی می‌گیرد و تراکنش ذخیره‌شده‌ی قبلی را روی آن اعمال می‌کند.

مثال، با این فرض که add_interest تراکنشی است که پنج واحد بهره واریز می‌کند:

remember_transaction(add_interest);
apply_remembered(100);
// => 105

remember_transaction(service_fee);
apply_remembered(100);
// => 98   (assuming service_fee deducts 2)

برای remember_transaction:

  • آرگومان، تراکنشی است که برای استفاده‌ی بعدی ذخیره می‌شود.
  • مقدار بازگشتی ندارد.

برای apply_remembered:

  • آرگومان یک عدد صحیح نامنفی ۶۴بیتی است.
  • مقدار بازگشتی یک عدد صحیح نامنفی ۶۴بیتی است.

2. دفترچه‌ی راهنمای بانک

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

دو تابع تعریف کنید که روی یک جدول ارسال که فراخواننده در اختیار می‌گذارد کار می‌کنند:

  • register_transaction آدرس حافظه‌ی یک جدول ارسال، یک اندیس و یک تراکنش می‌گیرد. این تراکنش را در اندیس داده‌شده در جدول ذخیره می‌کند.
  • select_transaction آدرس حافظه‌ی یک جدول ارسال، یک اندیس و یک موجودی می‌گیرد. تراکنش موجود در اندیس داده‌شده را پیدا می‌کند و آن را روی موجودی اعمال می‌کند و موجودی تازه را برمی‌گرداند.

select_transaction باید تراکنش پیداشده را تنها با یک فراخوانی دنباله‌ای غیرمستقیم فرا بخواند.

مثال، با این فرض که manual آدرس حافظه‌ی یک جدول ارسال با چهار جای خالی است:

register_transaction(manual, 0, monthly_interest);
register_transaction(manual, 1, service_fee);

select_transaction(manual, 0, 100);
// applies monthly_interest to 100

select_transaction(manual, 1, 100);
// applies service_fee to 100

برای register_transaction:

  • آرگومان اول آدرس حافظه‌ی یک جدول ارسال است.
  • آرگومان دوم یک عدد صحیح نامنفی ۶۴بیتی است (اندیس).
  • آرگومان سوم یک تراکنش است.
  • مقدار بازگشتی ندارد.

برای select_transaction:

  • آرگومان اول آدرس حافظه‌ی یک جدول ارسال است.
  • آرگومان دوم یک عدد صحیح نامنفی ۶۴بیتی است (اندیس).
  • آرگومان سوم یک عدد صحیح نامنفی ۶۴بیتی است (موجودی).
  • مقدار بازگشتی یک عدد صحیح نامنفی ۶۴بیتی است.

3. صورت حساب ماهانه را پردازش کنید

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

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

در شبه‌کد، process_statement(balance, transactions, n) این را محاسبه می‌کند:

for each transaction in transactions:
    balance = transaction(balance)
return balance

مثال، با این فرض که transactions آدرس حافظه‌ی آرایه‌ای است که تراکنش‌های add_interest، service_fee و add_interest را به همین ترتیب در خود دارد و add_interest پنج واحد اضافه می‌کند و service_fee دو واحد کم می‌کند:

process_statement(100, transactions, 3);
// add_interest(100) = 105
// service_fee(105)  = 103
// add_interest(103) = 108
// => 108

آرگومان اول یک عدد صحیح نامنفی ۶۴بیتی است. آرگومان دوم آدرس حافظه‌ی یک آرایه از تراکنش‌ها است. آرگومان سوم یک عدد صحیح نامنفی ۶۴بیتی است (طول آرایه). مقدار بازگشتی یک عدد صحیح نامنفی ۶۴بیتی است.

4. پردازش با یک نگهبان

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

تابع process_with_guard را تعریف کنید که یک موجودی اولیه، آدرس حافظه‌ی یک آرایه از تراکنش‌ها، تعداد تراکنش‌های آرایه و یک تابع نگهبان می‌گیرد. برای هر تراکنش به ترتیب:

  1. تراکنش را روی موجودی جاری اعمال کنید تا یک موجودی موقتی محاسبه شود.
  2. نگهبان را با موجودی موقتی فراخوانی کنید.
  3. اگر نگهبان مقدار ناصفر برگرداند، ثبت کنید: موجودی جاری همان موجودی موقتی می‌شود.
  4. اگر نگهبان صفر برگرداند، موجودی جاری بدون تغییر می‌ماند و تراکنش نادیده گرفته می‌شود.

پس از پردازش همه‌ی تراکنش‌ها، موجودی نهایی را همراه با تعداد تراکنش‌های تأییدشده برگردانید.

در شبه‌کد، process_with_guard(balance, transactions, n, guard) این را محاسبه می‌کند:

approved = 0
for each transaction in transactions:
    tentative = transaction(balance)
    if guard(tentative) is non-zero:
        balance = tentative
        approved = approved + 1
return balance, approved

برای نمونه، فرض کنید که:

  1. add_interest تراکنشی است که پنج واحد اضافه می‌کند و service_fee تراکنش دیگری است که دو واحد کم می‌کند
  2. at_least_10 نگهبانی است که وقتی موجودی ۱۰ یا بیشتر باشد مقدار ناصفر برمی‌گرداند

آنگاه:

process_with_guard(5, {add_interest, service_fee, add_interest}, 3, at_least_10);
// add_interest(5) = 10; at_least_10(10) != 0;
// => balance = 10, approved = 1
//
// service_fee(10) = 8; at_least_10(8) = 0;
// => balance = 10, approved = 1
//
// add_interest(10) = 15; at_least_10(15) != 0;
// => balance = 15, approved = 2
//
// final balance (15) is returned in rax
// number of approved transactions (2) is returned in rdx

برای process_with_guard:

  • آرگومان اول یک عدد صحیح نامنفی ۶۴بیتی است (موجودی اولیه).
  • آرگومان دوم آدرس حافظه‌ی یک آرایه از تراکنش‌ها است.
  • آرگومان سوم یک عدد صحیح نامنفی ۶۴بیتی است (طول آرایه).
  • آرگومان چهارم یک تابع نگهبان است که یک عدد صحیح نامنفی ۶۴بیتی می‌گیرد و یک عدد صحیح نامنفی ۶۴بیتی برمی‌گرداند.
  • مقادیر بازگشتی دو عدد صحیح نامنفی ۶۴بیتی هستند: موجودی نهایی در rax و تعداد تراکنش‌های تأییدشده در rdx.
ویرایش از طریق GitHub این لینک در پنجره یا زبانه‌ی جدیدی باز می‌شود
x86-64 Assembly Exercism

آماده‌اید حسابداری را شروع کنید؟

در Exercism ثبت‌نام کنید تا x86-64 Assembly را همراه با 22 مفهوم130 تمرین و مربی‌گری انسانی واقعی یاد بگیرید و در آن استاد شوید، همه‌ی این‌ها رایگان.