Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rust 和 Go 在图像处理上的简单对比 #23

Open
yangwenmai opened this issue Jun 14, 2019 · 2 comments
Open

Rust 和 Go 在图像处理上的简单对比 #23

yangwenmai opened this issue Jun 14, 2019 · 2 comments
Labels
practices question Further information is requested 博客

Comments

@yangwenmai
Copy link
Owner

yangwenmai commented Jun 14, 2019

背景

大家都说 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

image

还可以指定 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

注意:此样例代码,必须在 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 左右。

image

简单对比 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 模式下执行:

Cost in 550 ms

Rust decode && flip h 比 Go github.com/disintegration/imaging decode && flip h 要快 8 ms。

Go :

cost: 26.731724ms

Rust release 模式下:

Cost 17 ms

Go decode && flip h && save 比 Rust decode && flip h && save 要快 8 ms。

Go decode && flip h && save cost:

cost: 75.040754ms

Rust decode && flip h && save cost:

Cost in 81 ms

去掉 flip ,纯 image decode 然后再 save,则 Rust 比 Go 慢 10ms:

Go decode && save:

cost: 68.575711ms

Rust decode && save:

Cost in 77 ms

一探究竟

上面的代码中 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)) 执行:

cost: 89.767576ms

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();

参考资料

  1. https://github.com/image-rs/image
  2. https://github.com/golang/go#image
  3. Removing unnecessary copying in next_raw_interlaced_row image-rs/image-png#61
  4. Rust 和 Go 在图像处理上的性能之争
  5. Drawing a circle, but cost over 1 second, it's normal? #324

引用 wish:

语言层面 micro benchmark 还是挺多的,这些衡量语言本身性能的好坏应该足够了。至于库的话,生态也是语言的一部分,是工程中需要参考的因素。比如我觉得衡量 grpc go 性能和 grpc c core 性能差距得出语言性能差距,本身意义不大,但如果要用 grpc,那么是个很好的参考了。

我个人非常认同,语言好坏并不是一概而论的,有时候你得考虑更多方面,比方说:工程化、生态等。

@yangwenmai yangwenmai added question Further information is requested practices 博客 labels Jun 14, 2019
@upsuper
Copy link

upsuper commented Jun 15, 2019

如果是内存分配密集的应用可以换用jemalloc看看会不会有差异。

@yangwenmai yangwenmai pinned this issue Jun 16, 2019
@yangwenmai yangwenmai changed the title Rust 和 Go 在图像处理上的性能之争 Rust 和 Go 在图像处理上的简单对比 Jul 4, 2019
@artshell
Copy link

内存拷贝的问题,image-png 这个库后面得到了,优化,你再测测!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
practices question Further information is requested 博客
Projects
None yet
Development

No branches or pull requests

3 participants