برنامه نویسی

بهینه سازی رمزگشایی u128 از base62

در مقاله قبلی کدگذاری u128 به base62 را بررسی کردیم، اکنون عملیات معکوس رمزگشایی u128 را از base62 پیاده سازی و بهینه می کنیم، این می تواند برای مثال برای ذخیره سازی فشرده تر در حافظه یا پایگاه داده مفید باشد.

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

const BASE62_N: usize = 62;

pub fn base62_to_u128_naive(base62_str: &str) -> Option<u128> {
    let base62_str: [u8; 22] = base62_str.as_bytes().try_into().ok()?;
    let mut n: u128 = 0;
    for digit in &base62_str {
        let number = match digit {
            d if (b'0'..=b'9').contains(d) => d - 48,
            d if (b'A'..=b'Z').contains(d) => d - 55,
            d if (b'a'..=b'z').contains(d) => d - 61,
            _ => return None,
        };
        n = n
            .checked_mul(BASE62_N as u128)
            .and_then(|n| n.checked_add(number as u128))?;
    }
    Some(n)
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بیایید آنچه را که در اینجا اتفاق می افتد تجزیه و تحلیل کنیم، امضای تابع نشان می دهد که ما یک رشته را می پذیریم و در نتیجه یک عدد اختیاری u128 دریافت می کنیم، زیرا در صورت وجود داده های نادرست در رشته، نمی توانیم آن را رمزگشایی کنیم:

fn base62_to_u128_naive(base62_str: &str) -> Option<u128>
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

همه شناسه ها باید 22 کاراکتر باشند، این شرط را تأیید کنید:

let base62_str: [u8; 22] = base62_str.as_bytes().try_into().ok()?;
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

سپس، مقدار عدد را در یک حلقه از ارقام رشته base62 محاسبه می کنیم:

for digit in &base62_str
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

ما باید هر رقم را به اعشار تبدیل کنیم، برای مثال “B” در اعشار “11” خواهد بود، ما این کار را در match، بررسی هر یک از فواصل ممکن از کاراکترهای مجاز:

let number = match digit
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

حالا عدد را با ضرب در مبنا محاسبه می کنیم 62 و رقم بعدی را اضافه کنید. یک عدد 22 کاراکتری در پایه 62 اعداد بیشتری نسبت به یک عدد 128 بیتی دارد، بنابراین باید هنگام تبدیل، سرریز را بررسی کنیم:

n = n.checked_mul(BASE62_N as u128).and_then(|n| n.checked_add(number as u128))?;
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

ما نتیجه را گرفتیم، بازگشت:

Some(n)
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بیایید به نتایج بنچمارک نگاه کنیم، اولین نسخه از عملکرد چه چیزی را می‌تواند انجام دهد (CPU i5-4690):

bench_base62_to_u128_naive 1000000: 0.121s 125.75Mib/s, 8240895.48/s
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بد نیست، سرعت در حال حاضر کمی بیشتر از سرعت رمزگذار بهینه شده از مقاله اول است. همه به این دلیل است که در هنگام رمزگشایی عملیات تقسیم گران قیمت وجود ندارد، اما ما می توانیم بهتر انجام دهیم!

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

const BASE62_MAP: [u8; 123] = [
    255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
    255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
    255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 255, 255, 255,
    255, 255, 255, 255, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28,
    29, 30, 31, 32, 33, 34, 35, 255, 255, 255, 255, 255, 255, 36, 37, 38, 39, 40, 41, 42, 43, 44,
    45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61,
];
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

حالا برای تبدیل کافی است مقدار را بر اساس شاخص بگیریم، برای مثال، BASE62_MAP[b'A'] خواهد بود 10.

مرحله بعدی این است که با ضرب و جمع در یک حلقه مقابله کنید. این عملیات سرریز بررسی شده (checked انواع) به دستورالعمل‌های بیشتری نسبت به عملیات معمولی بدون علامت نیاز دارند. از آنجایی که سرریز فقط در مرحله آخر امکان پذیر است، می توانیم چک آن را با تبدیل تنها 21 کاراکتر اول در حلقه و آخرین کاراکتر بعد از حلقه به خارج از حلقه منتقل کنیم:

for digit in &base62_str[..21] {
        // ...
        n = n * BASE62_N as u128 + number as u128;
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

مشکل دیگر در اینجا این است که خود ضرب (و همچنین جمع) اعداد 128 بیتی گرانتر از مثلاً ضرب اعداد 64 بیتی است که با نگاه کردن به اسمبلر تولید شده برای هر دوی این عملیات به راحتی قابل مشاهده است. در مقاله قبلی تقسیم 128 بیتی را با تقسیم 64 بیتی جایگزین کردیم اما در اینجا برای ضرب و جمع همین کار را انجام می دهیم. برای انجام این کار، عدد base62 اصلی را به چندین بلوک در امتداد مرزهای رقمی تقسیم می کنیم، هر بلوک را جداگانه تبدیل می کنیم، سپس کل عدد را از این قسمت ها جمع آوری می کنیم:

3322222222221111111111 -> 33 2222222222 1111111111
                          n3         n2         n1
n = n3 * 62^20 + n2 * 62^10 + n1
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بنابراین نسخه بهینه شده نهایی رمزگشا که ما دریافت کردیم، به این صورت است:

const BASE62_N: usize = 62;
const BASE62_POW_10: u128 = (BASE62_N as u128).pow(10);
const BASE62_POW_20: u128 = (BASE62_N as u128).pow(20);
const BASE62_MAP: [u8; 123] = [
    255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
    255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
    255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 255, 255, 255,
    255, 255, 255, 255, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28,
    29, 30, 31, 32, 33, 34, 35, 255, 255, 255, 255, 255, 255, 36, 37, 38, 39, 40, 41, 42, 43, 44,
    45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61,
];

pub fn base62_to_u128(base62_str: &str) -> Option<u128> {
    let base62_str: [u8; 22] = base62_str.as_bytes().try_into().ok()?;
    let mut n_section: u64 = 0;
    for digit in &base62_str[12..22] {
        let number = *BASE62_MAP.get(*digit as usize).filter(|it| **it <= 61)?;
        n_section = n_section * BASE62_N as u64 + number as u64;
    }
    let mut n = n_section as u128;
    n_section = 0;
    for digit in &base62_str[2..12] {
        let number = *BASE62_MAP.get(*digit as usize).filter(|it| **it <= 61)?;
        n_section = n_section * BASE62_N as u64 + number as u64;
    }
    n += n_section as u128 * BASE62_POW_10;
    n_section = 0;
    for digit in &base62_str[0..2] {
        let number = *BASE62_MAP.get(*digit as usize).filter(|it| **it <= 61)?;
        n_section = n_section * BASE62_N as u64 + number as u64;
    }
    n.checked_add((n_section as u128).checked_mul(BASE62_POW_20)?)
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

چند توضیح برای کد در اینجا ما یک عدد را بر اساس بلوک های ارقام محاسبه می کنیم (ارقام کم اهمیت در انتهای یک رشته):

for digit in &base62_str[12..22]
// ...
for digit in &base62_str[2..12]
// ...
for digit in &base62_str[0..2]
// ..
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

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

let number = *BASE62_MAP.get(*digit as usize).filter(|it| **it <= 61)?;
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

ما مطمئن می شویم که 2 رقم بالا باعث سرریز نمی شوند:

n.checked_add((n_section as u128).checked_mul(BASE62_POW_20)?)
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بیایید معیار را بررسی کنیم، آیا ارزش تلاش ما را داشت؟

bench_base62_to_u128 10000000: 0.182s 836.24Mib/s, 54803747.40/s
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

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

لینک های من

من در حال ساخت برنامه یادداشت برداری Heaplist هستم، می توانید آن را در اینجا امتحان کنید heaplist.app
و من توییتر دارم @rsk

نوشته های مشابه

دیدگاهتان را بنویسید

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

دکمه بازگشت به بالا