在Reddit上还有关于Rust在基准测试中性能较差的讨论。
在我进行改进之前,代码需要16.81s才能完成基准测试并使用6.2Mb的内存,之后代码仅需要4.89s可以运行,但是内存消耗几乎没有变化。
如何改善代码并减少内存使用量?
更改后的文件也位于Github上。
use std::fs::File;
use std::path::Path;
use std::io::prelude::*;
use std::vec::Vec;
use std::io;
use std::env;
use std::collections::BTreeMap;
struct Tape {
pos: usize,
tape: Vec<isize>
}
impl Tape {
fn new() -> Tape { Tape { pos: 0, tape: vec![0] } }
fn get(&self) -> isize { self.tape[self.pos] }
fn getc(&self) -> u8 { self.get() as u8 }
fn inc(&mut self) { self.tape[self.pos] += 1; }
fn dec(&mut self) { self.tape[self.pos] -= 1; }
fn advance(&mut self) { self.pos += 1; if self.tape.len() <= self.pos { self.tape.push(0) } }
fn devance(&mut self) { if self.pos > 0 { self.pos -= 1; } }
}
struct Program {
code: Vec<u8>,
bracket_map: BTreeMap<usize, usize>
}
impl Program {
fn new(content: Vec<u8>) -> Program {
let mut code = Vec::new();
let mut bracket_map = BTreeMap::new();
let mut leftstack = Vec::new();
for (pc, b) in content.iter().filter(|&&x| x == b'+' || x == b'-' || x == b'.' || x == b','
|| x == b'<' || x == b'>' || x == b'[' || x == b']').map(|&x| x).enumerate() {
if b == b'[' {
leftstack.push(pc);
} else if b == b']' {
if let Some(left) = leftstack.pop() {
bracket_map.insert(left, pc);
bracket_map.insert(pc, left);
}
}
code.push(b);
}
Program{ code: code, bracket_map: bracket_map }
}
fn run(&self) {
let mut pc: usize = 0;
let mut tape = Tape::new();
let mut stdout = io::stdout();
while pc < self.code.len() {
match self.code[pc] {
b'+' => tape.inc(),
b'-' => tape.dec(),
b'>' => tape.advance(),
b'<' => tape.devance(),
b'[' => { if tape.get() == 0 { pc = self.bracket_map[&pc]; } },
b']' => { if tape.get() != 0 { pc = self.bracket_map[&pc]; } },
b'.' => { stdout.write(&[tape.getc()]).unwrap(); stdout.flush().unwrap() },
_ => unreachable!()
}
pc += 1;
}
}
}
fn main() {
let mut buf = Vec::new();
{
let arg1 = env::args().nth(1).unwrap();
let path = Path::new(&arg1);
let mut file = File::open(&path).unwrap();
file.read_to_end(&mut buf).unwrap();
}
Program::new(buf).run();
}
更多信息:该程序使用
rustc -C opt-level=3 brainfuck/brainfuck.rs -o brainfuck_rs
编译,内存使用情况由ruby脚本确定。 C ++的regading内存使用率更好,它仅需要1.6Mb。用于基准测试的Brainfuck rogram:
Benchmark brainf*ck program
>++[<+++++++++++++>-]<[[>+>+<<-]>[<+>-]++++++++
[>++++++++<-]>.[-]<<>++++++++++[>++++++++++[>++
++++++++[>++++++++++[>++++++++++[>++++++++++[>+
+++++++++[-]<-]<-]<-]<-]<-]<-]<-]++++++++++.
我每晚都在使用Rust在4个
rust-nightly-bin-1.2.0_2015.06.06-1
上运行的Arch Linux上的Intel(R) Core(TM) i5-2450M CPU @ 2.50GHz
。#1 楼
配置文件您只能改进您可以测量的范围。因此,首先让我们运行
callgrind
来检查我们大部分时间都花在了什么地方: $ rustc -C opt-level=3 -g brainfuck.rs
$ valgrind --tool=callgrind --dump-instr=yes --collect-jumps=yes --simulate-cache=yes ./brainfuck ahpla.bf
$ callgrind_annotate callgrind.out.*
我们最终将得到类似于以下配置文件的内容:
-------------------------------------------------------------------------------- Profile data file 'callgrind.out.29071' (creator: callgrind-3.11.0) -------------------------------------------------------------------------------- I1 cache: 32768 B, 64 B, 8-way associative D1 cache: 32768 B, 64 B, 8-way associative LL cache: 16777216 B, 64 B, 16-way associative Timerange: Basic block 0 - 10458028484 Trigger: Program termination Profiled target: ./brainfuck ahpla.bf (PID 29071, part 1) Events recorded: Ir Dr Dw I1mr D1mr D1mw ILmr DLmr DLmw Events shown: Ir Dr Dw I1mr D1mr D1mw ILmr DLmr DLmw Event sort order: Ir Dr Dw I1mr D1mr D1mw ILmr DLmr DLmw Thresholds: 99 0 0 0 0 0 0 0 0 Include dirs: User annotated: Auto-annotation: off -------------------------------------------------------------------------------- Ir Dr Dw I1mr D1mr D1mw ILmr DLmr DLmw -------------------------------------------------------------------------------- 49,487,884,719 13,086,961,954 6,269,022,579 2,411 3,281 1,553 2,167 2,109 1,422 PROGRAM TOTALS -------------------------------------------------------------------------------- Ir Dr Dw I1mr D1mr D1mw ILmr DLmr DLmw file:function -------------------------------------------------------------------------------- 16,380,206,099 5,113,395,424 1,588,909,048 12 4 1 12 4 . brainfuck.rs:brainfuck::main::h21788f29898deec2 [/home/zeta/misc/brainfuck] 14,242,423,355 2,080,022,568 3,380,036,673 2 0 0 2 . . /checkout/src/liballoc/btree/search.rs:alloc::btree::search::search_tree::h9cb21dc425abeb49 [/home/zeta/misc/brainfuck] 4,593,384,291 895,564,901 241 6 0 0 6 . . /checkout/src/liballoc/vec.rs:brainfuck::main::h21788f29898deec2 2,917,813,479 . . . . . . . . /checkout/src/libcore/slice/mod.rs:alloc::btree::search::search_tree::h9cb21dc425abeb49 2,426,711,760 808,903,920 0 1 1 0 1 . . /checkout/src/libcore/cmp.rs:alloc::btree::search::search_tree::h9cb21dc425abeb49 1,848,909,595 1,848,909,577 0 3 0 0 3 . . /checkout/src/liballoc/raw_vec.rs:brainfuck::main::h21788f29898deec2 1,560,016,932 780,008,466 260,002,823 1 1 0 1 . . /checkout/src/libcore/option.rs:brainfuck::main::h21788f29898deec2 1,560,016,926 520,005,642 0 1 0 0 1 . . /checkout/src/liballoc/btree/node.rs:alloc::btree::search::search_tree::h9cb21dc425abeb49 1,300,014,107 260,002,821 260,002,823 . . . . . . /checkout/src/liballoc/btree/map.rs:brainfuck::main::h21788f29898deec2 1,097,796,554 . . . . . . . . /checkout/src/libcore/iter/mod.rs:alloc::btree::search::search_tree::h9cb21dc425abeb49 1,040,011,284 260,002,821 780,008,463 . . . . . . /checkout/src/liballoc/btree/node.rs:brainfuck::main::h21788f29898deec2 260,003,033 260,002,823 207 0 0 3 0 0 1 /checkout/src/libcore/ptr.rs:brainfuck::main::h21788f29898deec2
您的程序执行了总共49,487,884,719条指令。其中只有16,380,206,099源自您的
main
,其余的源自其他功能。我们只能(半)直接控制30%的使用说明。不好。瓶颈瓶颈
不幸的是,
rustc
内联了Program::new
,Program::run
和所有Tape
函数。只是为了确保new()
不会占用程序的大部分内容,让我们删除run
一秒钟,然后再次检查:$ sed -i 's/.run();/;/' $ rustc -C opt-level=3 -g brainfuck.rs $ time ./brainfuck ahpla.bf real 0m0.002s user 0m0.004s sys 0m0.000s
正如我们可能已经猜到的那样,new()
几乎不需要时间。罪魁祸首是run()
和BTreeMap
。
内存使用情况
内存使用情况如何?好吧,这部分本身已经得到改善。您的代码在我的系统上使用1.6 MB。显然,BTreeMap
的实现在内存使用方面得到了很大改进。
使用rustfmt和clippy
虽然rustfmt
的输出取决于个人喜好,但clippy
可以提供有价值的输入。例如,map(|&x| x)
是cloned
。
对非瓶颈功能使用易于阅读的代码
您的filter
占new
的主导地位:
for (pc, b) in content.iter().filter(|&&x| x == b'+' || x == b'-' || x == b'.' || x == b',' || x == b'<' || x == b'>' || x == b'[' || x == b']').map(|&x| x).enumerate()
那是满口且难以维持的。既然这不是性能的关键部分,就让我们对其进行更改:
for (pc, b) in content.iter().filter(|x| b"+-,.[]<>".contains(x)).cloned().enumerate()
这很容易阅读,不是吗?
使用常量较低的\ $ \ O(1)\ $数据结构进行跳转
如果使用Vec
而不是BTreeMap
,我们可以大大减少时间。我们牺牲了一点内存,但这几乎没有引起注意。
BTreeMap
s被实现为B树。树始终具有某种程度的间接:您必须遍历树结构并比较键或值。但是BTreeMap
和std::map
在基本水平上有所不同:BTreeMap
做了额外的比较以在局部数组中找到正确的值,而std::map
只需要找到正确的node
。如果遍历整棵树,BTreeMap
的额外成本并不明显,但是如果我们一遍又一遍地寻找单个元素,这会很烦人,尤其是在小的数据集中。
所以让我们使用Vec
而是在我们使用它的同时采用所有其他建议:
struct Program { code: Vec<u8>, jumps: Vec<usize>, } impl Program { fn new(content: &[u8]) -> Program { let code: Vec<u8> = content .iter() .filter(|x| b"-+,.[]<>".contains(x)) .cloned() .collect(); let mut jumps = vec![0; code.len() + 1]; let mut leftstack = Vec::new(); for (pc, &b) in code.iter().enumerate() { if b == b'[' { leftstack.push(pc); } else if b == b']' { if let Some(left) = leftstack.pop() { jumps[left] = pc; jumps[pc] = left; } } } Program { code, jumps } } fn run(&self) { let mut pc: usize = 0; let mut tape = Tape::new(); let mut stdout = io::stdout(); while pc < self.code.len() { match self.code[pc] { b'+' => tape.inc(), b'-' => tape.dec(), b'>' => tape.advance(), b'<' => tape.devance(), b'[' => { if tape.get() == 0 { pc = self.jumps[pc]; } } b']' => { if tape.get() != 0 { pc = self.jumps[pc]; } } b'.' => { stdout.write(&[tape.getc()]).unwrap(); stdout.flush().unwrap() } _ => unreachable!(), } pc += 1; } } }
这如何影响您的变体?
$ ./bench.rb ./bf-cr ahpla.bf ZYXWVUTSRQPONMLKJIHGFEDCBA 3.10s, 1.5Mb $ ./bench.rb ./brainfuck-orig ahpla.bf ZYXWVUTSRQPONMLKJIHGFEDCBA 9.73s, 1.5Mb
请注意,bench.rb
报告的内存使用情况不稳定。无论哪种方式,我们都可以看到在这种情况下,Vec
不会占用任何明显的内存,但是可以极大地改善运行时间。
由于我们使用callgrind
进行了测量并发现,BTreeMap
特定的方法有助于运行时。
结束语
kostya后来添加了使用最小化AST的变体。那一个甚至更快,但是使用更多的内存,因为我们不再需要跳转了。但是,一个人可能可以改进它。
除了上述建议之外,您的代码还易于阅读和理解。干得好。
评论
您是否有理由认为可以进一步优化内存使用?当前的数字非常好,在当前时代,6 MB(我想您是说MB而不是Mb)可以忽略不计。我不太了解rust标准库,但是您可以使用基于哈希表的容器而不是BTreeMap来压缩几个周期(甚至可以节省内存)。
改进的很大一部分来自删除哈希表。散列太慢。
@glampert Yah; Rust的哈希表并不是特别快。
使用VecMap是明显的改进。在那之后,您可能需要编写一个JIT或实现一个优化的优化器。您也许可以避免一些不安全的分支,但这不太有帮助。