< 返回版块

祐祐 发表于 2021-08-27 00:17

Tags:println!, 效能

当把call函数注解后,或是注解println! 都可以快速运行。

在 https://play.rust-lang.org/ 上有时候可以 ""使用println! "" 而且依然编译的很快,有时候则不行,我自己本地电脑都不行。

这效能差了十万八千里,请大家帮忙,新手总是在 println! 跌坑。

这边使用 cargo run --release 编译

use std::time::{Duration, Instant};

struct Struct {
    a: String,
    b: bool,
}
trait Dyn {}
impl Dyn for Struct {}

fn main() {
    let start = Instant::now();
    let mut count = 0;
    let count_end = 100_000_000i64;

    while count <= count_end {
        let m: Box<Struct> = Box::new(Struct {
            b: false,
            a: "str".to_string(),
        });
        if count == count_end {
            call();               // ---- 这儿
            m.b;
            m.a;
        }
        count += 1;
    }

    let duration = start.elapsed();
    println!("Time: {:?}", duration);
}

fn call(){
    println!("run call()\n");     // ---- 重点在这儿,注解后变超快
}

Time:

😫使用println! 😄注解//println!
12.863911s 2.8486ms
13.2101748s 2.4661ms
13.5353751s 2.0433ms
13.4852107s 1.7869ms
———————— ————————

评论区

写评论
作者 祐祐 2021-08-28 11:14

已经有结论了

  1. 我支撑不起100000000 的堆内存分配😫,这样要耗时14秒。
  2. Rust 优化后的机器码已经面目全非,甚至是跳过释放内存和分配记内存规则,应该还会有更多。
  3. 自己优化程式胜过编译器的开心才帮你优化😂。
  4. 对于范例程式,println! 是关闭了优化没错。
作者 祐祐 2021-08-28 10:27

我也想看懂汇编,不过太难了,有工具可以帮忙把汇编转化的人性化点吗😂。

感謝 7sDream 的解說,我回去測了一段,既然是把 m 的堆分配沒了,那就一定可以寫出不使用 println! 仍然繞過編譯器優化。 我加入了...一樣不使用 println! 耗時 13s ,這裡不使用彙編情況下證明了 7sDream 的推斷(不會看汇编只好盲式繞過編譯氣優化)。

let mut a: Box<Struct> = Box::new(Struct {
      b: false,
      c: count - 1,
});
let mut b: Box<dyn Any> = Box::new(Struct {
      b: true,
      c: count - 2,
});
let mut m = if count % 2 == 0 { &mut a } else { &mut b };

--
👇
7sDream: 随便试了一个 Case:

其他的可以自己看。

这种问题不要靠猜测,看汇编是最快最有效的。

作者 祐祐 2021-08-28 10:08

感谢,又长知识了,原来lock还有这层用法。

--
👇
peacess: lock要在循环的外面,因为每一次调用print时都lock与unlock一次,这是它性能差的原因,所以要提高性能,要把lock放在循环外面,打印多次,只需要一次lock与unlock来提高效率,这样性会有提高。但是由于输出console的io耗时这个是没有办法的,除非不使用console输出。

peacess 2021-08-28 08:15

lock要在循环的外面,因为每一次调用print时都lock与unlock一次,这是它性能差的原因,所以要提高性能,要把lock放在循环外面,打印多次,只需要一次lock与unlock来提高效率,这样性会有提高。但是由于输出console的io耗时这个是没有办法的,除非不使用console输出。

--
👇
swdcgghxa: 按你的思路,式过了,和println!结果一样。

对了这是markdown 格式所以程式码要用三个 ` 将程式码包起来结束后也要有三个

--
👇
peacess: print的性能确实不行, 如果一定要使用,下面的方法,可以改进性能

use std::io::Write;
{
  let mut stdout = std::io::stdout();
  let mut lock = stdout.lock();
  for line in lines {
    writeln!(lock, "{}", line)?;
  }
}
Aya0wind 2021-08-27 17:25

你这println一旦注释掉,整个循环就完全无副作用了,要我是编译器,就会直接把整个循环都丢掉。
不过其实写了println理论上也可以优化,按理来说这个if里的内容能直接在编译时分析出执行完之后就退出循环,应该移到循环后面去,然后前面循环也因此无副作用,再把循环丢掉,就只剩一句call了(call也可以内联掉),可能是编译器还没这个水平。
开优化之后对运行结果和时间有疑问建议直接看汇编,你看rust代码是分析不出什么东西的,因为编译器可能会给你优化的面目全非。各个代码之间是有影响的,就算你发现你写了println程序变慢了,也不一定是println才导致慢,可能是因为你写了之后,某些优化条件不成立了,才导致慢。

7sDream 2021-08-27 15:37

随便试了一个 Case:

其他的可以自己看。

这种问题不要靠猜测,看汇编是最快最有效的。

7sDream 2021-08-27 15:27

你可以用 https://godbolt.org/ 这个网站看一下编译出来的汇编结果。

所有耗时在毫秒级和纳秒级别的 Case,堆分配(甚至是整个循环)应该都被优化了,否则速度是不可能那么快的。

作者 祐祐 2021-08-27 15:12

按你的思路,式过了,和println!结果一样。

对了这是markdown 格式所以程式码要用三个 ` 将程式码包起来结束后也要有三个

--
👇
peacess: print的性能确实不行, 如果一定要使用,下面的方法,可以改进性能

use std::io::Write;
{
  let mut stdout = std::io::stdout();
  let mut lock = stdout.lock();
  for line in lines {
    writeln!(lock, "{}", line)?;
  }
}
作者 祐祐 2021-08-27 15:07

我测试之后,效能延迟显然不是堆内存所导致的,而且回圈也似乎不是进行 初始状态到循环执行完毕的变化记录优化 ,因为按Rust 规则大括弧意味着新的作用域且结束后释放,我测试将其放入大括弧,但并没有比没大括弧有太多效能差距。

--
👇
madolchepalooza、7sDream

作者 祐祐 2021-08-27 14:55

我做了一次更改,让 m 变数一定会被修改,这效能快的很。

还特别在外围加上大跨号保证一定会在每次回圈中逐次执行,让他一定分配及销毁。

https://play.rust-lang.org/?version=stable&mode=release&edition=2018&gist=2e8668acb46b2c338dfb0a9d70384bd0

Time: 2.2517ms

Time: 2.6009ms

Time: 2.0037ms

Time: 3.3287ms

while count <= count_end {
    {
        let mut m: Box<Struct> = Box::new(Struct {
            b: false,
            a: "str".to_string(),
        });
        count += 1;
        if count == count_end {
            call(&mut m);
        }
    }
}

测一个不加上大挂号

https://play.rust-lang.org/?version=stable&mode=release&edition=2018&gist=75eee069d7dc1b2c659ed5d81fa6a348

Time: 2.9961ms

Time: 2.4795ms

Time: 2.5618ms

Time: 3.6779ms

while count <= count_end {
    let mut m: Box<Struct> = Box::new(Struct {
        b: false,
        a: "str".to_string(),
    });
    count += 1;
    if count == count_end {
        call(&mut m);
    }
}

测一个把 m 那行都取消掉,且有println!

https://play.rust-lang.org/?version=stable&mode=release&edition=2018&gist=e4207459c76b8946a31583d5b583b449

Time: 60.0766ms

Time: 64.3458ms

Time: 66.9781ms

Time: 72.9777ms

while count <= count_end {

    //let mut m: Box<Struct> = Box::new(Struct {
    //    b: false,
    //    a: "str".to_string(),
    //});
    count += 1;
    if count == count_end {
        //call(&mut m);
        println!("run");
    }
}

最后连println! 也都取消掉

https://play.rust-lang.org/?version=stable&mode=release&edition=2018&gist=6812812473cc8e7dca35063ab6c8b715

Time: 300ns

Time: 200ns

Time: 300ns

Time: 600ns

--
👇
madolchepalooza 👇 7sDream

madolchepalooza 2021-08-27 12:08

正如7sDream所说,如果把堆内存分配那句去掉,运行时间能够显著缩短;

一般来说循环优化一个比较显然的思路就是,只需要把其初始状态到循环执行完毕的变化记录下来就行,不必逐次执行;但如果在这个循环块中有print等等会修改外界存储状态(比如显存)的代码存在,那么就难以进行这种优化,因为不好推测这次修改是否依赖循环过程中产生的局部变量。而如果这种优化以函数为单位进行(只要函数非纯则不优化),那么如果在循环块中有非纯函数调用,就会导致这种情况发生。编译原理没学明白😂现查现用

其实我想说,效能什么的,C++的效能在现阶段还有不少优于Rust的地方;个人感觉Rust最大的优势就是满足较高的性能同时,基本没有历史包袱,能用上各种新的编程语言特性,而且还有Cargo等等先进的工具链;所以没必要单独因为性能问题决定是否要使用一种PL

--
👇
swdcgghxa: 什么意思,println!影响优化???

😭😭 那Rust 的效能是个惊喜吗,我就是冲着它这个效能来的。

就列印一次效能就没有了,尴尬 println!成本太高了,都别用println!除错了😂😂

--
👇
madolchepalooza: 感觉println只是个诱因...可能println这种IO行为把这个while块变成非纯的了,所以没法优化掉,只能逐一执行

毕竟你这里println理论上只执行一次啊😂 [[[ 4阿~~~~ ]]]

peacess 2021-08-27 11:58

print的性能确实不行, 如果一定要使用,下面的方法,可以改进性能

use std::io::Write; { let mut stdout = std::io::stdout(); let mut lock = stdout.lock(); for line in lines { writeln!(lock, "{}", line)?; } }

7sDream 2021-08-27 10:55

怀疑是你注释掉之后,这个函数就空了,所以编译器可以很有自信的判断出 Box 的那个变量从来没有被使用过,然后这个循环就直接优化掉了。

所以你这里最耗时的不是 println!,而是 Box 带来的堆上内存分配。

作者 祐祐 2021-08-27 10:30

什么意思,println!影响优化???

😭😭 那Rust 的效能是个惊喜吗,我就是冲着它这个效能来的。

就列印一次效能就没有了,尴尬 println!成本太高了,都别用println!除错了😂😂

--
👇
madolchepalooza: 感觉println只是个诱因...可能println这种IO行为把这个while块变成非纯的了,所以没法优化掉,只能逐一执行

毕竟你这里println理论上只执行一次啊😂 [[[ 4阿~~~~ ]]]

作者 祐祐 2021-08-27 10:18

程式这边写的是 if count == 100000000i64 所以只有跑一次,并没有列印很多行的问题。

👇 rayw0ng: 所有语言都有这个问题,尤其是终端渲染输出字符更费时。如果重定向输出到 /dev/null ,就会快很多。

👇 johnmave126: stdout输出确实挺慢的,println每行都会flush,加上你这个打印次数确实不少。不奇怪

madolchepalooza 2021-08-27 09:36

感觉println只是个诱因...可能println这种IO行为把这个while块变成非纯的了,所以没法优化掉,只能逐一执行

毕竟你这里println理论上只执行一次啊😂

rayw0ng 2021-08-27 08:58

所有语言都有这个问题,尤其是终端渲染输出字符更费时。如果重定向输出到 /dev/null ,就会快很多。

johnmave126 2021-08-27 01:35

stdout输出确实挺慢的,println每行都会flush,加上你这个打印次数确实不少。不奇怪

1 共 18 条评论, 1 页