- PVSM.RU - https://www.pvsm.ru -

Небольшая история о команде `yes` в Unix

Какую вы знаете самую простую команду Unix? Есть echo, которая печатает строку в stdout, и есть true, которая ничего не делает, а только завершается с нулевым кодом.

Среди множества простых Unix-команд спряталась команда yes. Если запустить её без аргументов, то вы получите бесконечный поток символов "y", каждый с новой строки:

y
y
y
y
(...ну вы поняли мысль)

Хотя на первый взгляд команда кажется бессмысленной, но иногда она бывает полезной:

yes | sh boring_installation.sh

Когда-нибудь устанавливали программу, которая требует ввести "y" и нажать Enter для установки? Команда yes приходит на помощь! Она аккуратно выполнит эту задачу, так что можете не отвлекаться от просмотра Pootie Tang [1].

Пишем yes

Вот базовая версия на… хм… BASIC.

10 PRINT "y"
20 GOTO 10

А вот то же самое на Python:

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);
  }
}

Некоторые пояснения:

  • Строка, которую мы печатаем в цикле, — это первый параметр командной строки под названием expletive. Это слово я узнал из руководства yes.
  • Я использую unwrap_or, чтобы получить expletive из параметров. Если параметры не установлены, по умолчанию используется "y".
  • Параметр по умолчанию конвертируется из строкового фрагмента (&str) в owned() в куче (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]

Упс, ничего особенно не улучшилось. Она даже медленнее, чем версия на Python! Это меня заинтересовало, так что я поискал исходники реализации на C.

Вот самая первая версия программы [2], которая вышла в составе Version 7 Unix за почётным авторством Кена Томпсона 10 января 1979 года:

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

Никакой магии.

Сравним со 128-строчной версией из комплекта GNU coreutils, зеркало которого есть на Github [3]. После 25 лет программа всё ещё в активной разработке! Последнее изменение кода произошло около года назад. Она довольно быстрая:

# 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, которая выбирается для каждой системы, чтобы максимально оптимизировать операции ввода-вывода (см. здесь [4]). На моей системе она была установлена как 1024 байта. В реальности лучшая производительность оказалась при 8192 байтах.

Я расширил свою программу Rust:

use std::io::{self, 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();
  }
}

Здесь важно, чтобы размер буфера делился на четыре, это гарантирует выравнивание в памяти [5].

Такая программа выдаёт 51,3 МиБ/с. Быстрее, чем версия, установленная в моей системе, но намного медленнее чем вариант от автора найденного мной поста на Reddit [6]. Он говорит, что добился скорости 10,2 ГиБ/с.

Дополнение

Как обычно, сообщество Rust не подкачало. Как только эта статья попала в подреддит о Rust [7], пользователь nwydo [8] указал на предыдущее обсуждение этой темы. Вот их оптимизированный код, который пробивает 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) -> Vec<u8> {
  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 [12].

Извлечённые уроки

Тривиальная программа yes на самом деле оказалась не такой простой. Для улучшения производительности в ней используется буферизация вывода и выравнивание памяти.

Переработка стандартных инструментов Unix — увлекательное занятие и оно заставляет ценить те изящные трюки, которые делают наши компьютеры быстрыми.

Автор: m1rko

Источник [13]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/open-source/267861

Ссылки в тексте:

[1] Pootie Tang: https://www.youtube.com/watch?v=yhBExhldRXQ

[2] самая первая версия программы: https://github.com/dspinellis/unix-history-repo/blob/4c37048d6dd7b8f65481c8c86ef8cede2e782bb3/usr/src/cmd/yes.c

[3] 128-строчной версией из комплекта GNU coreutils, зеркало которого есть на Github: https://github.com/coreutils/coreutils/blame/master/src/yes.c

[4] здесь: https://www.gnu.org/software/libc/manual/html_node/Controlling-Buffering.html

[5] выравнивание в памяти: https://stackoverflow.com/a/381368/270334

[6] поста на Reddit: https://www.reddit.com/r/unix/comments/6gxduc/how_is_gnu_yes_so_fast/

[7] подреддит о Rust: https://www.reddit.com/r/rust/comments/75fll1/a_little_story_about_the_yes_unix_command/

[8] nwydo: https://www.reddit.com/user/nwydo

[9] Поток стандартного вывода (stdout) защищён блокировкой: https://doc.rust-lang.org/std/io/struct.Stdout.html#method.lock

[10] std::ffi::OsString: https://doc.rust-lang.org/std/ffi/struct.OsString.html

[11] std::borrow::Cow: https://doc.rust-lang.org/std/borrow/enum.Cow.html

[12] убрать необязательный mut: https://github.com/cgati/yes/pull/3/files

[13] Источник: https://habrahabr.ru/post/342002/