- PVSM.RU - https://www.pvsm.ru -
Какую вы знаете самую простую команду Unix? Есть echo
, которая печатает строку в stdout, и есть true
, которая ничего не делает, а только завершается с нулевым кодом.
Среди множества простых Unix-команд спряталась команда yes
. Если запустить её без аргументов, то вы получите бесконечный поток символов "y", каждый с новой строки:
y
y
y
y
(...ну вы поняли мысль)
Хотя на первый взгляд команда кажется бессмысленной, но иногда она бывает полезной:
yes | sh boring_installation.sh
Когда-нибудь устанавливали программу, которая требует ввести "y" и нажать Enter для установки? Команда yes
приходит на помощь! Она аккуратно выполнит эту задачу, так что можете не отвлекаться от просмотра Pootie Tang [1].
Вот базовая версия на… хм… 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);
}
}
Некоторые пояснения:
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);
}
Так это же совсем другое дело!
std::ffi::OsString
[10] и std::borrow::Cow
[11], чтобы избежать ненужных размещений в памяти.
Единственное, что я могу добавить, так это убрать необязательный 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/
Нажмите здесь для печати.