Этот пример демонстрирует, что тривиальный случай write!(wr, "foo")
намного медленнее, чем вызов wr.write("foo".as_bytes())
:
extern mod extra;
use std::io::mem::MemWriter;
use extra::test::BenchHarness;
#[bench]
fn bench_write_value(bh: &mut BenchHarness) {
bh.iter(|| {
let mut mem = MemWriter::new();
for _ in range(0, 1000) {
mem.write("abc".as_bytes());
}
});
}
#[bench]
fn bench_write_ref(bh: &mut BenchHarness) {
bh.iter(|| {
let mut mem = MemWriter::new();
let wr = &mut mem as &mut Writer;
for _ in range(0, 1000) {
wr.write("abc".as_bytes());
}
});
}
#[bench]
fn bench_write_macro1(bh: &mut BenchHarness) {
bh.iter(|| {
let mut mem = MemWriter::new();
let wr = &mut mem as &mut Writer;
for _ in range(0, 1000) {
write!(wr, "abc");
}
});
}
#[bench]
fn bench_write_macro2(bh: &mut BenchHarness) {
bh.iter(|| {
let mut mem = MemWriter::new();
let wr = &mut mem as &mut Writer;
for _ in range(0, 1000) {
write!(wr, "{}", "abc");
}
});
}
Без оптимизации:
running 4 tests
test bench_write_macro1 ... bench: 280153 ns/iter (+/- 73615)
test bench_write_macro2 ... bench: 322462 ns/iter (+/- 24886)
test bench_write_ref ... bench: 79974 ns/iter (+/- 3850)
test bench_write_value ... bench: 78709 ns/iter (+/- 4003)
test result: ok. 0 passed; 0 failed; 0 ignored; 4 measured
С --opt-level=3
:
running 4 tests
test bench_write_macro1 ... bench: 62397 ns/iter (+/- 5485)
test bench_write_macro2 ... bench: 80203 ns/iter (+/- 3355)
test bench_write_ref ... bench: 55275 ns/iter (+/- 5156)
test bench_write_value ... bench: 56273 ns/iter (+/- 7591)
test result: ok. 0 passed; 0 failed; 0 ignored; 4 measured
Что мы можем сделать, чтобы это улучшить? Я могу придумать пару вариантов, но держу пари, что их больше:
write!
для компиляции в wr.write("foo".as_bytes())
. Если мы пойдем по этому пути, было бы неплохо также преобразовать серию str write!("foo {} {}", "bar", "baz")
.wr.write_str("foo")
. Насколько я понимаю, это блокируется на №6164.write!
. Есть ли функции, которые должны быть встроены, а не встроенные? Моя попытка разброса не дала никаких результатов.Я подозреваю, что fn bench_write_ref
не совершает виртуальных звонков. Добавление этого
#[inline(never)]
fn writer_write(w: &mut Writer, b: &[u8]) {
w.write(b);
}
#[bench]
fn bench_write_virt(bh: &mut BenchHarness) {
bh.iter(|| {
let mut mem = MemWriter::new();
let wr = &mut mem as &mut Writer;
for _ in range(0, 1000) {
writer_write(wr, "abc".as_bytes());
}
});
}
У меня с --opt-level 3
running 5 tests
test bench_write_macro1 ... bench: 680823 ns/iter (+/- 34497)
test bench_write_macro2 ... bench: 950790 ns/iter (+/- 72309)
test bench_write_ref ... bench: 505846 ns/iter (+/- 41965)
test bench_write_value ... bench: 511815 ns/iter (+/- 36681)
test bench_write_virt ... bench: 553466 ns/iter (+/- 43716)
Итак, виртуальный вызов имеет влияние, но он не полностью объясняет медлительность write!()
показанную этим стендом.
Посещение для выявления ошибок. Кажется, это все еще присутствует. Код для запуска тестов теперь:
extern crate test;
use std::io::MemWriter;
use test::Bencher;
#[bench]
fn bench_write_value(bh: &mut Bencher) {
bh.iter(|| {
let mut mem = MemWriter::new();
for _ in range(0u, 1000) {
mem.write("abc".as_bytes());
}
});
}
#[bench]
fn bench_write_ref(bh: &mut Bencher) {
bh.iter(|| {
let mut mem = MemWriter::new();
let wr = &mut mem as &mut Writer;
for _ in range(0u, 1000) {
wr.write("abc".as_bytes());
}
});
}
#[bench]
fn bench_write_macro1(bh: &mut Bencher) {
bh.iter(|| {
let mut mem = MemWriter::new();
let wr = &mut mem as &mut Writer;
for _ in range(0u, 1000) {
write!(wr, "abc");
}
});
}
#[bench]
fn bench_write_macro2(bh: &mut Bencher) {
bh.iter(|| {
let mut mem = MemWriter::new();
let wr = &mut mem as &mut Writer;
for _ in range(0u, 1000) {
write!(wr, "{}", "abc");
}
});
}
Без оптимизаций:
running 4 tests
test bench_write_macro1 ... bench: 1470468 ns/iter (+/- 291966)
test bench_write_macro2 ... bench: 1799612 ns/iter (+/- 316293)
test bench_write_ref ... bench: 1336574 ns/iter (+/- 251664)
test bench_write_value ... bench: 1317880 ns/iter (+/- 254668)
test result: ok. 0 passed; 0 failed; 0 ignored; 4 measured
С --opt-level=3
:
running 4 tests
test bench_write_macro1 ... bench: 127671 ns/iter (+/- 1452)
test bench_write_macro2 ... bench: 196158 ns/iter (+/- 2053)
test bench_write_ref ... bench: 43881 ns/iter (+/- 453)
test bench_write_value ... bench: 43859 ns/iter (+/- 336)
test result: ok. 0 passed; 0 failed; 0 ignored; 4 measured
По-прежнему проблема с использованием следующего кода:
#![allow(unused_must_use)]
#![feature(test)]
extern crate test;
use std::io::Write;
use std::vec::Vec;
use test::Bencher;
#[bench]
fn bench_write_value(bh: &mut Bencher) {
bh.iter(|| {
let mut mem = Vec::new();
for _ in 0..1000 {
mem.write("abc".as_bytes());
}
});
}
#[bench]
fn bench_write_ref(bh: &mut Bencher) {
bh.iter(|| {
let mut mem = Vec::new();
let wr = &mut mem as &mut Write;
for _ in 0..1000 {
wr.write("abc".as_bytes());
}
});
}
#[bench]
fn bench_write_macro1(bh: &mut Bencher) {
bh.iter(|| {
let mut mem = Vec::new();
let wr = &mut mem as &mut Write;
for _ in 0..1000 {
write!(wr, "abc");
}
});
}
#[bench]
fn bench_write_macro2(bh: &mut Bencher) {
bh.iter(|| {
let mut mem = Vec::new();
let wr = &mut mem as &mut Write;
for _ in 0..1000 {
write!(wr, "{}", "abc");
}
});
}
Обычно дают что-то вроде следующего:
$ cargo bench
running 4 tests
test bench_write_macro1 ... bench: 21,604 ns/iter (+/- 82)
test bench_write_macro2 ... bench: 29,273 ns/iter (+/- 85)
test bench_write_ref ... bench: 1,396 ns/iter (+/- 387)
test bench_write_value ... bench: 1,391 ns/iter (+/- 163)
Примерно такие же результаты я получил с ржавчиной по ночам.
Просто для записи, запускается с v1.20.0-nightly
:
$ cargo bench
running 4 tests
test bench_write_macro1 ... bench: 36,556 ns/iter (+/- 69)
test bench_write_macro2 ... bench: 54,377 ns/iter (+/- 958)
test bench_write_ref ... bench: 13,730 ns/iter (+/- 24)
test bench_write_value ... bench: 13,755 ns/iter (+/- 81)
Сегодня:
running 4 tests
test bench_write_macro1 ... bench: 16,220 ns/iter (+/- 982)
test bench_write_macro2 ... bench: 25,542 ns/iter (+/- 2,220)
test bench_write_ref ... bench: 4,889 ns/iter (+/- 314)
test bench_write_value ... bench: 4,819 ns/iter (+/- 956)
Два года спустя:
running 4 tests
test bench_write_macro1 ... bench: 17,561 ns/iter (+/- 174)
test bench_write_macro2 ... bench: 23,285 ns/iter (+/- 2,771)
test bench_write_ref ... bench: 3,234 ns/iter (+/- 194)
test bench_write_value ... bench: 3,238 ns/iter (+/- 123)
test result: ok. 0 passed; 0 failed; 0 ignored; 4 measured; 0 filtered out
❯ rustc +nightly --version
rustc 1.47.0-nightly (30f0a0768 2020-08-18)
Мне было любопытно, была ли разница в реализации io :: Write и fmt :: Write или в деталях write_fmt, требующих format_args !, что актуально с момента записи! можно попросить "обслужить" в любом случае.
Чтобы узнать больше об этом, я сравнил Vec со String, сравнение вроде бы «яблоки с яблоками ... иш». https://gist.github.com/workingjubilee/2d2e3a7fded1c2101aafb51dc79a7ec5
running 10 tests
test string_write_fmt ... bench: 10,053 ns/iter (+/- 1,141)
test string_write_macro1 ... bench: 10,177 ns/iter (+/- 2,363)
test string_write_macro2 ... bench: 17,499 ns/iter (+/- 1,847)
test string_write_ref ... bench: 2,270 ns/iter (+/- 265)
test string_write_value ... bench: 2,333 ns/iter (+/- 126)
test vec_write_fmt ... bench: 15,722 ns/iter (+/- 1,673)
test vec_write_macro1 ... bench: 15,767 ns/iter (+/- 1,638)
test vec_write_macro2 ... bench: 23,968 ns/iter (+/- 8,942)
test vec_write_ref ... bench: 2,296 ns/iter (+/- 178)
test vec_write_value ... bench: 2,230 ns/iter (+/- 235)
test result: ok. 0 passed; 0 failed; 0 ignored; 10 measured; 0 filtered out
Мне показалось интересным, что этот пример показал, что у String на самом деле меньше накладных расходов, чем у Vec на write!
(тест имел высокую дисперсию, но я счел его достаточно показательным).
Потом я посмотрел и увидел:
fn write_fmt(&mut self, fmt: fmt::Arguments<'_>) -> Result<()> {
// Create a shim which translates an io::Write to a fmt::Write and saves
// off I/O errors. instead of discarding them
struct Adaptor<'a, T: ?Sized + 'a> {
inner: &'a mut T,
error: Result<()>,
}
/* More code related to implementing and using the resulting shim,
* seemingly involving a lot of poking a reference at runtime??? */
}
Так что я больше не удивлен.
Я считаю, что в некоторых случаях можно ускорить процесс, но на самом деле это не сравнение яблок с яблоками. Оранжевый - это write_fmt
, который должен использовать различные форматирующие машины. Что должно произойти, так это то, что write!
должно пропустить write_fmt
в простых случаях, когда механизм форматирования не требуется, или механизм форматирования должен быть намного быстрее в случае очевидного быстрого пути.