لینک کوتاه مطلب : https://hsgar.com/?p=5876

داستانی کوچک در مورد فرمان «yes» یونیکس

ساده ترین دستور یونیکس که می شناسید چیست؟
وجود دارد echo، که یک رشته را به stdout و چاپ می کند true، که همیشه با کد خروج 0 خاتمه می یابد.

در میان سری دستورات ساده یونیکس، نیز وجود دارد yes. اگر آن را بدون آرگومان اجرا کنید، یک جریان نامتناهی از y دریافت می کنید که با یک خط جدید از هم جدا می شوند:

y
y
y
y
(...you get the idea)

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

yes | sh boring_installation.sh

آیا تا به حال برنامه ای را نصب کرده اید که برای ادامه کار باید “y” را تایپ کنید و اینتر را بزنید؟ yes برای نجات! وظیفه خود را با دقت انجام می دهد، بنابراین می توانید به تماشای آن ادامه دهید پوتی تانگ.

نوشتن بله

در اینجا یک نسخه اولیه در … اوه … BASIC است.

10 PRINT "y"
20 GOTO 10

و اینجا همان چیزی است که در پایتون وجود دارد:

while True:
    print("y")

ساده، نه؟ نه خیلی سریع!
به نظر می رسد، آن برنامه بسیار کند است.

python yes.py | pv -r > /dev/null
[4.17MiB/s]

آن را با نسخه داخلی مک من مقایسه کنید:

yes | pv -r > /dev/null
[34.2MiB/s]

بنابراین سعی کردم یک نسخه سریعتر در Rust بنویسم. این اولین تلاش من است:

use std::env;

fn main() {
  let expletive = env::args().nth(1).unwrap_or("y".into());
  loop {
    println!("{}", expletive);
  }
}

چند توضیح:

  • رشته ای که می خواهیم در یک حلقه چاپ کنیم، اولین پارامتر خط فرمان است و نام گذاری شده است جنجالی. من این کلمه را از yes صفحه مدیریت
  • من استفاده می کنم unwrap_or برای بدست آوردن جنجالی از پارامترها در صورتی که پارامتر تنظیم نشده باشد، از “y” به عنوان پیش فرض استفاده می کنیم.
  • پارامتر پیش فرض از یک قطعه رشته تبدیل می شود (&str) به یک متعلق به ریسمان روی پشته (String) استفاده كردن into().

بیایید آن را آزمایش کنیم.

cargo run --release | pv -r > /dev/null
   Compiling yes v0.1.0
    Finished release [optimized] target(s) in 1.0 secs
     Running `target/release/yes`
[2.35MiB/s]

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

اینجاست نسخه اول برنامه، منتشر شده با نسخه 7 یونیکس و نویسنده معروف Ken Thompson در 10 ژانویه 1979:

main(argc, argv)
char **argv;
{
  for (;;)
    printf("%sn", argc>1? argv[1]: "y");
}

اینجا جادو نیست

آن را با نسخه 128 خطی از Coreutils گنو که در Github منعکس شده است. پس از 25 سال، هنوز در حال توسعه فعال است! آخرین تغییر کد در حدود a اتفاق افتاد سال پیش. این خیلی سریع است:

# brew install coreutils
gyes | pv -r > /dev/null
[854MiB/s]

بخش مهم در پایان این است:

/* Repeatedly output the buffer until there is a write error; then fail.  */
while (full_write (STDOUT_FILENO, buf, bufused) == bufused)
  continue;

آها! بنابراین آنها به سادگی از یک بافر برای سریعتر کردن عملیات نوشتن استفاده می کنند. اندازه بافر با یک ثابت به نام تعریف می شود BUFSIZ، که در هر سیستم به گونه ای انتخاب می شود که I/O را کارآمد کند (نگاه کنید به اینجا). در سیستم من، 1024 بایت تعریف شد. من در واقع عملکرد بهتری با 8192 بایت داشتم.

من برنامه Rust خود را تمدید کردم:

use std::env;
use std::io::{self, BufWriter, Write};

const BUFSIZE: usize = 8192;

fn main() {
    let expletive = env::args().nth(1).unwrap_or("y".into());
    let mut writer = BufWriter::with_capacity(BUFSIZE, io::stdout());
    loop {
        writeln!(writer, "{}", expletive).unwrap();
    }
}

بخش مهم این است که برای اطمینان از اندازه بافر مضربی از چهار است تراز حافظه.

در حال اجرا که به من 51.3 مگابایت بر ثانیه داد. سریعتر از نسخه ای که با سیستم من ارائه می شود، اما همچنان بسیار کندتر از نتایج بدست آمده است این پست ردیت که من پیدا کردم، جایی که نویسنده در مورد 10.2GiB/s صحبت می کند.

به روز رسانی

بار دیگر، جامعه Rust ناامید نشد.
به محض این پست زیر ردیت Rust را بزنید، کاربر nwydo اشاره کرد الف بحث قبلی در همین موضوع در اینجا کد بهینه شده آنها است که علامت 3 گیگابایت در ثانیه را در دستگاه من می شکند:

use std::env;
use std::io::{self, Write};
use std::process;
use std::borrow::Cow;

use std::ffi::OsString;
pub const BUFFER_CAPACITY: usize = 64 * 1024;

pub fn to_bytes(os_str: OsString) -> Vecu8> {
  use std::os::unix::ffi::OsStringExt;
  os_str.into_vec()
}

fn fill_up_buffer'a>(buffer: &'a mut [u8], output: &'a [u8]) -> &'a [u8] {
  if output.len() > buffer.len() / 2 {
    return output;
  }

  let mut buffer_size = output.len();
  buffer[..buffer_size].clone_from_slice(output);

  while buffer_size  buffer.len() / 2 {
    let (left, right) = buffer.split_at_mut(buffer_size);
    right[..buffer_size].clone_from_slice(left);
    buffer_size *= 2;
  }

  &buffer[..buffer_size]
}

fn write(output: &[u8]) {
  let stdout = io::stdout();
  let mut locked = stdout.lock();
  let mut buffer = [0u8; BUFFER_CAPACITY];

  let filled = fill_up_buffer(&mut buffer, output);
  while locked.write_all(filled).is_ok() {}
}

fn main() {
  write(&env::args_os().nth(1).map(to_bytes).map_or(
    Cow::Borrowed(
      &b"yn"[..],
    ),
    |mut arg| {
      arg.push(b'n');
      Cow::Owned(arg)
    },
  ));
  process::exit(1);
}

حالا این یک بازی کاملا متفاوت است!

تنها چیزی که می توانستم کمک کنم این بود حذف غیر ضروری mut. 😅

درس های آموخته شده

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

لینک منبع

ارسال یک پاسخ

آدرس ایمیل شما منتشر نخواهد شد.