Поглиблене порівняння stdout і stderr: буферизація, продуктивність та приклади в Rust (Частина 2)

14.09.2025 8 хвилин Автор: Lady Liberty

У другій частині ми детальніше розбираємо, як різні методи буферизації впливають на роботу stdout і stderr. Автор експериментує з LineWriter, BufWriter та необробленим виводом, демонструючи різницю у швидкості виконання. З’ясувалося, що коли обидва потоки використовують однакову техніку буферизації, їхня продуктивність стає майже ідентичною. Додатково розглянуто приклади використання from_raw_fd для створення небуферизованого stdout, а також можливості оптимізації у TUI-додатках. Порівняння з іншими мовами програмування — Go, Python, C, C++ та Zig — показує, що підхід до стандартних потоків може відрізнятися, але принципи буферизації залишаються ключовими. Це поглиблене дослідження допомагає зрозуміти, коли варто залишати stderr «сирим», а коли доцільно застосовувати буферизацію для кращої швидкодії.

Перевірка теорії буферизації

Давайте згадаємо зміни, які ми зробили та які привели нас до цього моменту:

-let mut terminal = Terminal::new(CrosstermBackend::new(std::io::stdout()))?;
+let mut terminal = Terminal::new(CrosstermBackend::new(std::io::stderr()))?;

Отже, щось внутрішнє змінюється, коли ми це робимо. ratatuiЦе CrosstermBackendгарне місце, куди варто звернути увагу в першу чергу:

Структура CrosstermBackend— це обгортка навколо реалізованого автора Write, яка використовується для надсилання команд до терміналу. Вона надає методи для малювання контенту, маніпулювання курсором та очищення екрана терміналу.

/// A Backend implementation that uses Crossterm to render to the terminal.
pub struct CrosstermBackend<W: Write> {
    writer: W,
}

impl<W> CrosstermBackend<W>
where
    W: Write,
{
    pub fn new(writer: W) -> CrosstermBackend<W> {
        CrosstermBackend { writer }
    }
}

Write— це особливість Rust для об’єктів, які можна використовувати для запису потоків байтів. Вона має такі методи, як writeflushі, що найважливіше write_all, з якими ми стикалися раніше. Наведений вище код означає, що CrosstermBackendможе працювати з будь-чим, що реалізує , Writeтакі як File&mut [8]та інші типи, включаючи Stdout. Ця абстракція допомагає нам використовувати бекенд з різними структурами.

Ось чому компілятор не скаржиться, коли ми змінюємо аргумент з Stdoutна Stderr.

Добре, отже, ratatuiвикористовує stdout як спосіб запису та надсилає його на crosstermзапис. У цьому випадку нам потрібно заглибитися на один рівень, щоб crosstermпобачити, що відрізняється від stdout та stderr.

Насправді ні. Погляньте на цю діаграму:

У міру заглиблення в дослідження, можливості переданого типу обмежуються реалізацією. Іншими словами, якщо ви приймаєте параметр Write, у вас є лише набір функціональних можливостей, з якими ви можете працювати ( writewrite_allтощо). У цьому випадку crosstermне має жодного шансу вказати stdout з stderr діяти по-іншому, оскільки він знає лише про “тип”, який є write. Ось чому нам потрібно заглиблюватися , з цього ми й почали, зі структурами Stdout та Stderr .

Зачекайте, ви маєте на увазі, що нам потрібно перевірити вихідний код стандартної бібліотеки Rust, щоб отримати відповідь на це питання? Хіба це не означає, що stdout не завжди швидший за stderr, і це залежить від деталей реалізації?

Саме так. «Все є файлом», пам’ятаєте? stdout та stderr також є файлами, які нічим не відрізняються. Rust, мабуть, творить якусь магію в цьому випадку. 🪄

Щоб розібратися в магії, можна звернутися до визначення Stdout:

pub struct Stdout {
    // FIXME: this should be LineWriter or BufWriter depending on the state of
    //        stdout (tty or not). Note that if this is not line buffered it
    //        should also flush-on-panic or some form of flush-on-abort.
    inner: &'static ReentrantMutex<RefCell<LineWriter<StdoutRaw>>>,
}

std/src/io/stdio.rs#L535-L540

Тепер подивимося Stderr:

pub struct Stderr {
    inner: &'static ReentrantMutex<RefCell<StderrRaw>>,
}

std/src/io/stdio.rs#L778-L780

Погляньте на різницю:

-ReentrantMutex<RefCell<LineWriter<StdoutRaw>>>
+ReentrantMutex<RefCell<StderrRaw>>

Хм… Отже Stdout, додатково обгорнуто в іншу структуру під назвою LineWriter.

Так, бачите, до чого це все йде?

LineWriterобгортає блок запису та буферизує вивід до нього, скидаючи дані щоразу, коли виявляється символ нового рядка ( 0x0a, ).'\n' Ми можемо використовувати LineWriterдля запису по одному рядку за раз, що значно зменшує кількість фактичних записів у файл.

Це те, чого ми весь цей час шукали. Давайте спробуємо!

use std::io::{self, LineWriter, Write};
use std::thread;
use std::time::Duration;

let stdout = io::stdout();
let mut writer = LineWriter::new(stdout);

writer.write_all(b"In Rust's domain where choices gleam,")?;
eprintln!("[waiting for newline]");
thread::sleep(Duration::from_secs(1));

// No bytes are written until a newline is encountered
// (or the internal buffer is filled).
writer.write_all(b"\n")?;
eprintln!("\n[writing the rest]");
thread::sleep(Duration::from_secs(1));

// Write the rest.
writer.write_all(
    b"Ratatui's path, a unique stream.
Terminal canvas, colors bright,
Untraveled road, a different light.
That choice, the difference, in code's delight.",
)?;

// The last line doesn't end in a newline,
// so we have to flush or drop the `LineWriter` to finish writing.
eprintln!("\n[flush or drop to finish writing]");
thread::sleep(Duration::from_secs(1));
writer.flush()?;

linewriter.rs

Якщо ми запустимо цей код:

[waiting for newline]
In Rust's domain where choices gleam,

[writing the rest]
Ratatui's path, a unique stream.
Terminal canvas, colors bright,
Untraveled road, a different light.

[flush or drop to finish writing]
That choice, the difference, in code's delight.

З цього виводу ми можемо спостерігати:

  • Перше eprintln!повідомлення виводиться, і модуль запису чекає на символ нового рядка для запису в stdout (хоча ми вже викликали його write_allраніше).

  • Друга частина вірша друкується цілком до останнього рядка.

  • Останній рядок не друкується, доки ми не очистимо стандартний вивід.

Хоча на перший погляд це може здатися досить дивною поведінкою, насправді вона має величезну перевагу в продуктивності, і саме тому stdout набагато швидший за stderr! До зустрічі в іншій публікації в блозі.

Зачекай! Це воно?

Ви маєте рацію, ми можемо зробити більше з цією інформацією.

Експерименти з буферизованим записом

Повернемося до стандартного визначення бібліотеки Stdout:

pub struct Stdout {
    // FIXME: this should be LineWriter or BufWriter depending on the state of
    //        stdout (tty or not). Note that if this is not line buffered it
    //        should also flush-on-panic or some form of flush-on-abort.
    inner: &'static ReentrantMutex<RefCell<LineWriter<StdoutRaw>>>,
}

std/src/io/stdio.rs#L535-L540

Так, а що FIXMEтам із коментарем? А що таке BufWriter?

Гарні питання, документація дуже добре пояснює BufWriter :

Зберігає BufWriterбуфер даних в пам’яті та записує їх на базовий записувач великими, нечастими пакетами. BufWriterЦе може підвищити швидкість програм, які здійснюють невеликі та повторювані виклики запису до одного й того ж файлу або мережевого сокета. Іншими словами, BufWriterзаписує дані у свій внутрішній буфер замість фактичного потоку, а потім нечасто записує ці зібрані дані в потік.

Хіба це не те саме, що й LineWriter?

Гарне зауваження! Вони насправді відрізняються, коли йдеться про скидання (тобто, коли буферизовані дані записуються в потік).

  • BufWriter: скидається, коли внутрішній буфер заповнений.

  • LineWriter: така ж поведінка, як і BufWriter, але також зчищає для кожного рядка (коли виявлено 0x0aабо ).\n

Також вони обидва червоніють, коли автор виходить за межі поля зору.

Щоб було зрозуміліше:

Давайте змінимо наш попередній LineWriterприклад на використання BufWriter:

use std::io::{self, BufWriter, Write};
use std::thread;
use std::time::Duration;

let stdout = io::stdout();
let mut writer = BufWriter::new(stdout);

writer.write_all(b"In Rust's domain where choices gleam,")?;
eprintln!("[writing the first line]");
thread::sleep(Duration::from_secs(1));

// No bytes are written until a newline is encountered
// (or the internal buffer is filled).
writer.write_all(b"\n")?;
eprintln!("\n[writing the rest]");
thread::sleep(Duration::from_secs(1));

// Write the rest.
writer.write_all(
    b"Ratatui's path, a unique stream.
Terminal canvas, colors bright,
Untraveled road, a different light.
That choice, the difference, in code's delight.",
)?;

// The last line doesn't end in a newline,
// so we have to flush or drop the `LineWriter` to finish writing.
eprintln!("\n[flush or drop to finish writing]");
thread::sleep(Duration::from_secs(1));
writer.flush()?;

bufwriter.rs

Ми побачимо, що нічого не друкується, доки ми не скинемо BufWriter:

[writing the first line]

[writing the rest]

[flush or drop to finish writing]
In Rust's domain where choices gleam,
Ratatui's path, a unique stream.
Terminal canvas, colors bright,
Untraveled road, a different light.
That choice, the difference, in code's delight.

Що стосується коментаря до структури stdout:

Це має бути LineWriter або BufWriter, залежно від стану stdout (tty чи ні). Тут йдеться про те, що stdout повинен автоматично вибирати міжбуферизація рядківLineWriter) табуферизація блоківBufWriter) залежно від того, чи підключено воно до TTY чи ні. Отже, stdout більше не повинен буферизуватися в рядки під час виведення на нетермінал (як-от пересилання виводу у файл).

Я не зрозумів, яка перевага використання BufWriterв цьому випадку?

У випадку не-TTY, скажімо, запису у файл, це означатиме, що вивід буде буферизований блоками, тому ми не будемо робити системні виклики для кожного рядка. Іншими словами, ми не будемо постійно скидати дані, що є величезною перевагою в продуктивності. Це може позбавити нас значних накладних витрат під час роботи з файлами більшого розміру.

І це насправді реалізовано в Rust, але запит на злиття не об’єднано: #115652

Також було б чудово мати можливість увімкнути буферизацію блоків для stdout у майбутньому. Відповідне обговорення є тут: #60673

Яка була б від цього користь?

Перегляньте, наприклад, цей код:

for i in 1..1000000 {
    println!("{}", i);
}

Майте на увазі, що println!це записує в stdout, і він буферизується в рядки (тобто використовує LineWriter) за замовчуванням. Іншими словами, він очищає термінал для кожного рядка та виконує системний виклик!

А тепер погляньте на це:

let stdout = io::stdout();
let mut output = BufWriter::new(stdout);
for i in 1..1000000 {
    writeln!(output, "{}", i)?;
}

Тут ми обгортаємо stdout в a BufWriter, що робить його блочно буферизованим ✨

#!/usr/bin/env rust-script

use std::{
    io::{self, BufWriter, Result, Write},
    time::Instant,
};

fn main() -> Result<()> {
    let first = Instant::now();
    for i in 1..1000000 {
        println!("{}", i);
    }
    let first_elapsed = first.elapsed();

    let second = Instant::now();
    let stdout = io::stdout();
    let mut output = BufWriter::new(stdout);
    for i in 1..1000000 {
        writeln!(output, "{}", i)?;
    }
    let second_elapsed = second.elapsed();
    output.flush()?;

    println!("Line buffered: {:?}", first_elapsed);
    println!("Block buffered: {:?}", second_elapsed);

    Ok(())
}

Коли ми його запускаємо:

$ chmod +x block-buffered-stdout.rs

$ ./block-buffered-stdout.rs

# [...]
Line buffered: 1.080789949s
Block buffered: 408.105636ms

Блокування буферизованих stout-запусків~2 рази швидше!

Чудово! Цікаво, що станеться, якщо ми застосуємо це до нашого застосунку TUI.

Ми можемо спробувати зробити таку зміну:

-let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
+let mut terminal = Terminal::new(CrosstermBackend::new(BufWriter::new(stdout())))?;

Хм, помітного збільшення продуктивності немає. Що, якби ми спробували щось більш суттєве, наприклад, використали…блоковий буферизований stderr. За замовчуванням stderr не буферизується, пам’ятаєте?

Так… О! У мене є краща ідея. Як щодо того, щоб зробити рядковий буфер stderr? Stdout також буферизується рядками, тож чи отримаємо ми таку ж продуктивність?

Так, давайте спробуємо це!

// line buffered stdout (as default)
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;

// line buffered stderr
let mut terminal = Terminal::new(CrosstermBackend::new(LineWriter::new(stderr())))?;

Блін, невже ми щойно зробили продуктивність stderr ідентичною stdout, просто зробивши її рядково-буферизованою?

О так, схоже на це!

Експерименти з необробленими записами

Як щодо того, щоб зробити протилежне тому, що ми робили досі, і спробувати зробити stdout небуферизованим ? Це погіршить продуктивність, і ми, ймовірно, отримаємо результат, подібний до використання stderr за замовчуванням. Давайте доведемо нашу гіпотезу!

Якщо ми подивимося на наші висновки на даний момент:

Оскільки stderr()function повертає необроблений потік за замовчуванням (тобто StderrRaw), легше реалізувати буферний шар поверх нього. Однак, stdout()функція вже повертає буферизований потік, тому нам потрібно якимось чином отримати необроблений потік.

Якщо ви пам’ятаєте зміст Stdout:

/// A handle to the global standard output stream of the current process.
pub struct Stdout {
    inner: &'static ReentrantMutex<RefCell<LineWriter<StdoutRaw>>>,
}

std/src/io/stdio.rs#L535-L540

Нам потрібен StdoutRawдля небуферизованого потоку, а не для його обгортання всередині LineWriter. Визначення типу також підтверджує поведінку без буферизації:

/// A handle to a raw instance of the standard output stream of this process.
///
/// This handle is not synchronized or buffered in any fashion. Constructed via
/// the `std::io::stdio::stdout_raw` function.
struct StdoutRaw(stdio::Stdout);

std/src/io/stdio.rs#L45-L49

Чудово. Цей коментар підводить нас до stdout_rawфункції:

/// Constructs a new raw handle to the standard output stream of this process.
///
/// The returned handle has no external synchronization or buffering layered on
/// top.
const fn stdout_raw() -> StdoutRaw {
    StdoutRaw(stdio::Stdout::new())
}

std/src/io/stdio.rs#L69-L81

Легко, ми можемо просто створити необроблений стандартний вивід через виклик stdout_raw!

Не зовсім, це приватна дія.

std::io::stdio::stdout_raw();
         ^^^^^  ---------- function `stdout_raw` is not publicly re-exported
         |
         private module

Насправді існує проблема відстеження з 2019 року щодо розкриття необроблених stdout/stderr/stdin: #58326

Наразі не існує простого/очевидного способу отримати небуферизований Stdout/err/in. Ці типи існують у stdio, проте вони не є публічними з незгаданих причин. Наприклад, ці типи були б корисними для CLI-додатків, які записують багато даних одночасно без їх зайвого очищення. І, на жаль, наразі досі немає простого/очевидного способу отримати небуферизовані потоки вводу/виводу 🙁

Але!

У цьому випуску ми маємо деякі підказки щодо можливих обхідних шляхів. Одна річ, яка повторюється кілька разів у випуску, полягає в тому, що ми можемо використовувати це from_raw_fdв Linux як обхідний шлях.

Дайте здогадаюсь, from_raw_fdприймає файловий дескриптор, і ми просто збираємося використовувати файловий дескриптор stdout (який дорівнює “1”) для створення небуферизованого потоку.

Саме так!

use std::fs::File;

let mut raw_stdout = File::from_raw_fd(1);
writeln!(raw_stdout, "test");

Але…

error[E0133]: call to unsafe function is unsafe and requires unsafe function or block
   |
55 |                 let raw_stdout = File::from_raw_fd(1);
   |                                  ^^^^^^^^^^^^^^^^^^^^ call to unsafe function
   |
   = note: consult the function's documentation for information on how to avoid undefined behavior

Якщо ми прочитаємо документацію :from_raw_fd

Безпека: fdПереданий файл має бути власним файловим дескриптором; зокрема, він має бути відкритим.

Є більше деталей (які я розгляну в іншій публікації блогу), але мораль цієї історії полягає в тому, що нам потрібно помістити наш код у unsafeблок ось так:

use std::fs::File;

// SAFETY: no other functions should call `from_raw_fd`, so there
// is only one owner for the file descriptor.
let raw_stdout = unsafe { File::from_raw_fd(1) };
writeln!(raw_stdout, "test");

Якщо ви його запустите, то побачите “test” на стандартному виводі. Ура! \o/

Однак, як коротко згадувалося в попередньому розділі, з цим кодом все ще існує велика проблема. Повертаючись до документації from_raw_fd:

Ця функція зазвичай використовується для використання права власності на вказаний файловий дескриптор. При такому використанні повернений об’єкт візьме на себе відповідальність за його закриття , коли об’єкт виходить за межі області видимості.

Це означає, що raw_stdoutзмінна отримує право власності на файловий дескриптор і закриває стандартний вивід (stdout), коли той виходить за межі області видимості. Іншими словами, коли створений об’єкт Fileвидаляється, стандартний вивід закривається.

Ми можемо підтвердити цю поведінку за допомогою цього коду:

use std::fs::File;
use std::io::{Result, Write};
use std::os::fd::FromRawFd;

fn print1() -> Result<()> {
    let mut raw_stdout = unsafe { File::from_raw_fd(1) };
    writeln!(raw_stdout, "test1")
}

fn print2() -> Result<()> {
    let mut raw_stdout = unsafe { File::from_raw_fd(1) };
    writeln!(raw_stdout, "test2")
}

fn main() -> Result<()> {
    print1()?;
    print2()?;
    Ok(())
}

raw-stdout-broken.rs

Ви очікуєте побачити “test1” та “test2”, проте stdout закривається після того, як ми залишаємо першу функцію. Коли ми намагаємося відкрити її знову, вона викличе паніку через правило безпеки ( fdпереданий файл має бути власним дескриптором файлу + він має бути відкритим ).

$ ./raw-stdout-broken.rs

test1
Error: Os { code: 9, kind: Uncategorized, message: "Bad file descriptor" }

Це погано. Що нам робити?

У нашому випадку ми хочемо, щоб відкритий файл зберігався протягом усієї програми. Також припустимо, що це TUI-програма, і в нас є окремі функції, куди передача raw_stdoutзначення неможлива.

Що ж, є ще один швидкий брудний спосіб вирішення проблеми: ліниво ініціалізувати stdout та зробити його глобально доступним через lazy_static(або інший крейт, наприклад once_cell):

use std::fs::File;
use std::io::{Result, Write};
use std::os::fd::FromRawFd;
use std::sync::Mutex;

lazy_static! {
    static ref RAW_STDOUT: Mutex<File> = unsafe { Mutex::new(File::from_raw_fd(1)) };
}

fn print1() -> Result<()> {
    writeln!(RAW_STDOUT.lock().unwrap(), "test1")
}

fn print2() -> Result<()> {
    writeln!(RAW_STDOUT.lock().unwrap(), "test2")
}

fn main() -> Result<()> {
    print1()?;
    print2()?;
    Ok(())
}

raw-stdout-1.rs

$ ./raw-stdout-1.rs

test1
test2

Гаразд, гаразд, це трохи забагато. Я маю на увазі лінивий, статичний, м’ютекс, блокування тощо… Хіба у нас немає кращого способу вирішити це? Крім того, за допомогою цього коду неможливо створити жодних інших екземплярів stdout.

Насправді є кращий спосіб. Документація FromRawFdдає нам підказку:

Споживання права власності не є суворо обов’язковим. Використовуйте From<OwnedFd>::fromреалізацію для API, яка суворо споживає право власності.

Схоже, ми можемо обійти закриття стандартного виводу, якщо використаємо OwnedFd.

Власний файловий дескриптор. Це закриває файловий дескриптор під час видалення. Гарантується, що ніхто інший не закриє файловий дескриптор.

Тож ми можемо створити ✨глобальний небуферизований стандартний вивід, який не використовує права власності на базовий файловий дескриптор✨ ось так:

use lazy_static::lazy_static;
use std::fs::File;
use std::io::{Result, Write};
use std::os::fd::{FromRawFd, OwnedFd};

lazy_static! {
    static ref RAW_STDOUT_FD: OwnedFd = unsafe { OwnedFd::from_raw_fd(1) };
}

fn print1() -> Result<()> {
    let mut raw_stdout = File::from(RAW_STDOUT_FD.try_clone()?);
    writeln!(raw_stdout, "test1")
}

fn print2() -> Result<()> {
    let mut raw_stdout = File::from(RAW_STDOUT_FD.try_clone()?);
    writeln!(raw_stdout, "test2")
}

fn main() -> Result<()> {
    print1()?;
    print2()?;
    Ok(())
}
$ ./raw-stdout-2.rs

test1
test2

Якщо нам потрібне більш елегантне рішення, ми можемо використовувати as_raw_fdфункцію Stdoutзамість простого “1”:

static ref RAW_STDOUT_FD: OwnedFd = {
    let stdout = std::io::stdout();
    let raw_fd = stdout.as_raw_fd();
    unsafe { OwnedFd::from_raw_fd(raw_fd) }
};

Весь цей біль, чому?

Щоб ми могли це зробити:

let stdout = std::io::stdout();
let raw_fd = stdout.as_raw_fd();
let raw_stdout = unsafe { File::from_raw_fd(raw_fd) };

// initialize the terminal with raw/unbuffered stdout
let mut terminal = Terminal::new(CrosstermBackend::new(BufWriter::new(raw_stdout)))?;

Так, я мало не забув, що ми займаємося TUI. Здається, початкове питання було: «Чи необроблений stdout такий же повільний, як необроблений stderr?».

Так, давайте це з’ясуємо:

І на цьому ми завершуємо, необроблений stdout має таку ж продуктивність, як і необроблений stderr.

Пришвидшення stout

Усе, що ми зробили досі, викликає питання:Чи можемо ми зробити stdout швидшим?

Що ж, тепер ми знаємо, що причина, чому stderr повільніший за stdout, полягає в тому, що він не буферизується . Тож чи можемо ми якимось чином досягти швидшого (більш продуктивного) вводу-виводу за допомогою stdout, зробивши щось на кшталт «кращої буферизації»?

Ще один спосіб покращити FPS – це зменшити кількість writeвикликів. Однак, crosstermratatuiвже виконує деякі оптимізації, наприклад, не рендерить комірки, які не змінюються. Проблема полягає в тому, що в прикладі лічильника FPS, який ми використовуємо, комірки постійно змінюються, тому ця оптимізація не має жодного ефекту. Крім того, ми встановлюємо кольори як фону, так і переднього плану, тому кожен рендеринг по суті займає кілька writeвикликів.

На скріншоті нижче я змінив crosstermбекенд, ratatuiщоб виділити комірки, які не змінюються між рендерами. З кількості червоних клітин чітко видно, що ми не можемо пропустити багато writeвикликів, оскільки майже все змінюється в терміналі:

Однак це не стосується більшості програм TUI, і ця оптимізація фактично позбавляє нас необхідності повторно відображати більшу частину екрана.

Ще один цікавий момент, на який варто звернути увагу, це розмір буфера. З меншим розміром буфера (100 байт) та затримкою між рендерингами ми можемо спостерігати наступне:

Тоді як більший буфер рендерить більші фрагменти:

Ми не побачимо великої різниці, якщо приберемо режим сну, хіба що якщо використовуватимемо дуже малий/великий буфер, тоді FPS значно падає. Ми, мабуть, можемо поекспериментувати з розміром буфера, щоб малювати одну лінію для кожного рендерингу, але мені не вдалося отримати кращий FPS у моїх спробах.

Ще один момент, який варто зазначити, це нещодавні розробки, спрямовані ratatuiна покращення продуктивності рендерингу комірок. Однак це не має великого впливу на FPS, але однозначно є покращенням завдяки використанню менших ресурсів.

Ми можемо продовжувати експериментувати з низькорівневими функціями crosstermratatuiдля подальшої оптимізації, але мені здається, що це була б краща тема для другої частини цієї публікації.

Під час написання цього мені не вдалося досягти “швидшого stdout”, тому не соромтеся залишати коментарі щодо своїх пропозицій!

Результати

Ось порівняння ratatuiрендерингу crosstermдля stdout та stderr з використанням небуферизованого / рядково-буферизованого / блочно-буферизованого запису:

stdout-vs-stderr-all.rs

Висновок з цього полягає в тому, що потоки вводу/виводу мають подібну продуктивність, коли використовується однакова техніка буферизації. Також можна сказати, std::io::stdout()щошвидшеніж std::io::stderr()через використання рядкової буферизації проти її відсутності.

Підписатися
Сповістити про
0 Коментарі
Найстаріші
Найновіше Найбільше голосів
Знайшли помилку?
Якщо ви знайшли помилку, зробіть скріншот і надішліть його боту.