背景
大家都说 Rust 比较擅长系统底层,我猜想图像处理还是很底层的。
至少比较好用的都是 C 语言实现的。
imagemagick
libpng ? 也是 C 实现的。
那我们是不是可以来测试一下 Rust 和 Go 在图像处理上的表现呢?首先从 decode 开始。
Rust decode 一个图片文件
for _ in 0..10 {
let timer = Instant::now();
let tiny = image::open("examples/scaleup/out0.png").unwrap();
println!("cost: {}", Elapsed::from(&timer));
}
耗时:
Decode in ~1.73 s
Rust 指定 Release 模式下运行 Decode 耗时 :
Decode in ~21 ms
还可以指定 opt-level3
Go decode 一个图片文件
startTime := time.Now()
data, err := ioutil.ReadFile("out0.png")
if err != nil {
panic(err)
}
rd := bytes.NewReader(data)
image.Decode(rd)
fmt.Println("cost:", time.Now().Sub(startTime))
耗时:695.732µs
当时我就震惊了!!!
注意看上面的代码 image.Decode(rd)
这里有 error 返回,但是这里测试代码没有捕获。
其实它会报错:
panic: image: unknown format
代码修改为:
for i := 0;i < 10; i++ {
startTime := time.Now()
data, err := ioutil.ReadFile("rust.png")
if err != nil {
panic(err)
}
rd := bytes.NewReader(data)
_,_,err = image.Decode(rd)
if err != nil {
panic(err)
}
fmt.Println("耗时:", time.Now().Sub(startTime))
}
使用 png 解析就正常了:
for i := 0;i < 10; i++ {
startTime := time.Now()
data, err := ioutil.ReadFile("rust.png")
if err != nil {
panic(err)
}
rd := bytes.NewReader(data)
_, err = png.Decode(rd)
if err != nil {
panic(err)
}
fmt.Println("耗时:", time.Now().Sub(startTime))
}
执行耗时:
耗时: ~15.914074ms
Rust 和 Go 在对 png 图片进行 decode 时,两者的耗时差别并不大。
当我们将图片更换为 jpeg 后,他们的对比如下:
Rust(RELEASE模式下):
Decode in 3 ms
Go:
耗时: ~5.472894ms
对于 jpeg 的图片,Rust decode 要稍稍优于 Go 的 jpeg decode。
分析讨论过程
通过看 image 的源码发现 png 这个库 next frame 这个方法比较慢。
go版本一次性读整个图,png要一行一行的读,且每行都要一次内存拷贝
为了更高的抽象层级,有非常多细碎的内存拷贝
找到原因了:每行会创建一个Vec,一次Vec创建的时间在几十微秒左右,一个几百行的图片,主要会花在内存分配上
(准确说,单纯创建Vec不会发生堆内存分配,等价于一个栈上变量,代价可以忽略,但随后会对其写入,此时就会导致堆内存分配)
其实怎么存都有问题,抛开内存分配的问题,flatten到一维,行序,列序在处理的时候都对cache不友好
分析2:
主要不是内存分配的问题,其实在初始化的时候已经通过宏得到了图片大小,一次性分配好了。
主要是内存copy的问题,那里还注释了 TODO 待优化。
内存copy,还有下面那一行into转换,内存会重新分配吧,作者打算留给有缘人优化了。
Rust不保证代码的性能。
初学者用rust比较难写出高性能的程序吧,但是用go可以好一点。
应该是 初学者用rust比较难写出程序
写不好rust是我不行,不是rust不行。
很多人误以为,用rust写了代码就性能好了
其实我的印象里,内存拷贝的成本应该比内存分配低?
不过至少可以确定,image 这个库的速度确实是慢😂
我还测试了一下 jpeg 的解码,发现速度也一样糟糕
没法复用,他api设计的时候就断了复用的念头了
关键是后面解码的时候remalloc
读Row是个公开api,返回的是字节序列引用
作者还是有考虑的,可能处理时候有点问题,还没细看
Rust 还是一个新手,所以源代码和实现逻辑还得仔细研究研究再来理解大家的讨论了。
其他
环境:
MacBookPro 2017
3.1 GHz Intel Core i7
16 GB 2133 MHz LPDDR3
rustc 1.36.0-nightly (33fe1131c 2019-04-20)
cargo 1.36.0-nightly (b6581d383 2019-04-16)
Go 1.12.4
Rust 画一个○
使用 image-rs/imageproc
在一个 1000x1000 的画板上画一个 500x500 的圆:
//! An example using the drawing functions. Writes to the user-provided target file.
use std::env;
use std::path::Path;
use std::fmt;
use std::time::{Duration, Instant};
use image::{Rgb, RgbImage};
use imageproc::rect::Rect;
use imageproc::drawing::{
draw_cross_mut,
draw_line_segment_mut,
draw_hollow_rect_mut,
draw_filled_rect_mut,
draw_hollow_circle_mut,
draw_filled_circle_mut
};
struct Elapsed(Duration);
impl Elapsed {
fn from(start: &Instant) -> Self {
Elapsed(start.elapsed())
}
}
impl fmt::Display for Elapsed {
fn fmt(&self, out: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match (self.0.as_secs(), self.0.subsec_nanos()) {
(0, n) if n < 1000 => write!(out, "{} ns", n),
(0, n) if n < 1000_000 => write!(out, "{} µs", n / 1000),
(0, n) => write!(out, "{} ms", n / 1000_000),
(s, n) if s < 10 => write!(out, "{}.{:02} s", s, n / 10_000_000),
(s, _) => write!(out, "{} s", s),
}
}
}
fn main() {
let arg = if env::args().count() == 2 {
env::args().nth(1).unwrap()
} else {
panic!("Please enter a target file path")
};
let timer = Instant::now();
let path = Path::new(&arg);
let white = Rgb([255u8, 255u8, 255u8]);
let mut image = RgbImage::new(1000, 1000);
// Draw a filled circle within bounds
draw_filled_circle_mut(&mut image, (500, 500), 400, white);
image.save(path).unwrap();
println!("draw in {}", Elapsed::from(&timer));
}
Debug:
Output: draw in 1.22s
Release:
Output:draw in 20ms
注意:此样例代码,必须在 image-rs/imageproc/examples/drawing.rs
中运行。单独运行会报错:
error[E0277]: the trait bound `image::buffer::ImageBuffer<image::color::Rgb<u8>, std::vec::Vec<u8>>: image::image::GenericImage` is not satisfied
--> src/main.rs:48:5
|
48 | draw_filled_circle_mut(&mut image, (500, 500), 400, white);
| ^^^^^^^^^^^^^^^^^^^^^^ the trait `image::image::GenericImage` is not implemented for `image::buffer::ImageBuffer<image::color::Rgb<u8>, std::vec::Vec<u8>>`
|
= note: required by `imageproc::drawing::conics::draw_filled_circle_mut`
Go 画一个○
package main
import (
"bytes"
"fmt"
"github.com/fogleman/gg"
"image"
"io"
"io/ioutil"
"time"
)
func main() {
startTime := time.Now()
dc := gg.NewContext(1000, 1000)
dc.DrawCircle(500, 500, 400)
dc.SetRGB(0, 0, 0)
dc.Fill()
dc.SavePNG("out.png")
// 99.617772ms ~ 108.1321ms
fmt.Println("耗时:", time.Now().Sub(startTime))
}
耗时:99ms ~ 108ms 左右。
简单对比 Rust 比 Go 快 5 倍。(这个还是非常值得期待的,但是两者图像不太一样,所以还需要修补修补)
以上代码:https://github.com/developer-learning/learning-rust/tree/master/practices/image
翻转我的头像文件(不生成文件)
使用 github.com/disintegration/imaging
库:
package main
import (
"fmt"
"log"
"time"
"github.com/disintegration/imaging"
)
func main() {
for i := 0; i < 10; i++ {
startTime := time.Now()
img, err := imaging.Open("avatar-origin.jpg")
if err != nil {
log.Fatalln(err)
return
}
imaging.FlipH(img)
fmt.Println("cost:", time.Now().Sub(startTime))
}
}
使用 Rust 的 image crate 源代码:
extern crate image;
use image::{FilterType, PNG};
use std::fmt;
use std::fs::File;
use std::time::{Duration, Instant};
struct Elapsed(Duration);
impl Elapsed {
fn from(start: &Instant) -> Self {
Elapsed(start.elapsed())
}
}
impl fmt::Display for Elapsed {
fn fmt(&self, out: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match (self.0.as_secs(), self.0.subsec_nanos()) {
(0, n) if n < 1000 => write!(out, "{} ns", n),
(0, n) if n < 1000_000 => write!(out, "{} µs", n / 1000),
(0, n) => write!(out, "{} ms", n / 1000_000),
(s, n) if s < 10 => write!(out, "{}.{:02} s", s, n / 10_000_000),
(s, _) => write!(out, "{} s", s),
}
}
}
fn main() {
for _ in 0..10 {
let timer = Instant::now();
let tiny = image::open("examples/scaleup/avatar-origin.jpg").unwrap();
tiny.fliph();
println!("Decode in {}", Elapsed::from(&timer));
}
}
执行10次,耗时:
非 release 模式下执行:
Rust decode && flip h 比 Go github.com/disintegration/imaging
decode && flip h 要快 8 ms。
Go :
Rust release 模式下:
Go decode && flip h && save 比 Rust decode && flip h && save 要快 8 ms。
Go decode && flip h && save cost:
Rust decode && flip h && save cost:
去掉 flip ,纯 image decode 然后再 save,则 Rust 比 Go 慢 10ms:
Go decode && save:
Rust decode && save:
一探究竟
上面的代码中 Go decode && flip h && save 比 Rust 快 8-10 ms,我们也已经知道差距是在 save。
所以我们研究一下 Go 和 Rust 的 save 部分代码。
Go 代码:
var defaultEncodeConfig = encodeConfig{
jpegQuality: 95,
gifNumColors: 256,
gifQuantizer: nil,
gifDrawer: nil,
pngCompressionLevel: png.DefaultCompression,
}
很明显 Go save 的时间比较小是因为 jpegQuality
默认是 95 ,所以指定 jpegQuality
为 100: err = imaging.Save(img, "newavatar-origin-flip-h.jpg", imaging.JPEGQuality(100))
执行:
Go 整体执行时间比 Rust 多 3-5ms。
缩放
Go:
img = imaging.Fit(img, 200, 200, imaging.Lanczos)
Rust:
let mut d = tiny.resize(200, 200, FilterType::Lanczos3);
旋转
Go:
img = imaging.Rotate(img, -90, color.RGBA{0, 0, 0, 0})
Rust:
let mut d = tiny.rotate90();
参考资料
- https://github.com/image-rs/image
- https://github.com/golang/go#image
- image-rs/image-png#61
- Rust 和 Go 在图像处理上的性能之争
- Drawing a circle, but cost over 1 second, it's normal? #324
引用 wish:
语言层面 micro benchmark 还是挺多的,这些衡量语言本身性能的好坏应该足够了。至于库的话,生态也是语言的一部分,是工程中需要参考的因素。比如我觉得衡量 grpc go 性能和 grpc c core 性能差距得出语言性能差距,本身意义不大,但如果要用 grpc,那么是个很好的参考了。
我个人非常认同,语言好坏并不是一概而论的,有时候你得考虑更多方面,比方说:工程化、生态等。