【问题标题】:Why don't I get performance improvement by using get_unchecked()?为什么使用 get_unchecked() 不能提高性能?
【发布时间】:2016-08-28 23:08:30
【问题描述】:

我尝试使用get_unchecked() 而不是[] 索引运算符来提高我的des crate 的s() 函数的性能。

但是,即使 [](或 get_unchecked())函数在我的基准测试中被调用了 48 亿次,它也不会带来明显的性能提升。我原以为调用 get_unchecked() 48 亿次而不是 [] 会导致我的 Intel Core 2 Duo 2.4 GHz 处理器的时间提高 2 秒。

我做了这个小基准,有一个小代码给你看:

#![feature(test)]

extern crate test;

fn main() {
}

pub fn s(box_id: usize, block: u64) -> u64 {
    const TABLES: [[[u64; 16]; 4]; 8] =
        [[[ 14,  4, 13, 1,  2, 15, 11,  8,  3, 10,  6, 12,  5,  9, 0, 7]
        , [  0, 15,  7, 4, 14,  2, 13,  1, 10,  6, 12, 11,  9,  5, 3, 8]
        , [  4,  1, 14, 8, 13,  6,  2, 11, 15, 12,  9,  7,  3, 10, 5, 0]
        , [ 15, 12,  8, 2,  4,  9,  1,  7,  5, 11,  3, 14, 10,  0, 6, 13]
        ],
        [ [ 15,  1,  8, 14,  6, 11,  3,  4,  9, 7,  2, 13, 12, 0,  5, 10]
        , [  3, 13,  4,  7, 15,  2,  8, 14, 12, 0,  1, 10,  6, 9, 11,  5]
        , [  0, 14,  7, 11, 10,  4, 13,  1,  5, 8, 12,  6,  9, 3,  2, 15]
        , [ 13,  8, 10,  1,  3, 15,  4,  2, 11, 6,  7, 12,  0, 5, 14,  9]
        ],
        [ [ 10,  0,  9, 14, 6,  3, 15,  5,  1, 13, 12,  7, 11,  4,  2,  8]
        , [ 13,  7,  0,  9, 3,  4,  6, 10,  2,  8,  5, 14, 12, 11, 15,  1]
        , [ 13,  6,  4,  9, 8, 15,  3,  0, 11,  1,  2, 12,  5, 10, 14,  7]
        , [  1, 10, 13,  0, 6,  9,  8,  7,  4, 15, 14,  3, 11,  5,  2, 12]
        ],
        [ [  7, 13, 14, 3,  0,  6,  9, 10,  1, 2, 8,  5, 11, 12,  4, 15]
        , [ 13,  8, 11, 5,  6, 15,  0,  3,  4, 7, 2, 12,  1, 10, 14,  9]
        , [ 10,  6,  9, 0, 12, 11,  7, 13, 15, 1, 3, 14,  5,  2,  8,  4]
        , [  3, 15,  0, 6, 10,  1, 13,  8,  9, 4, 5, 11, 12,  7,  2, 14]
        ],
        [ [  2, 12,  4,  1,  7, 10, 11,  6,  8,  5,  3, 15, 13, 0, 14,  9]
        , [ 14, 11,  2, 12,  4,  7, 13,  1,  5,  0, 15, 10,  3, 9,  8,  6]
        , [  4,  2,  1, 11, 10, 13,  7,  8, 15,  9, 12,  5,  6, 3,  0, 14]
        , [ 11,  8, 12,  7,  1, 14,  2, 13,  6, 15,  0,  9, 10, 4,  5,  3]
        ],
        [ [ 12,  1, 10, 15,  9,  2,  6,  8,  0, 13,  3,  4, 14,  7,  5, 11]
        , [ 10, 15,  4,  2,  7, 12,  9,  5,  6,  1, 13, 14,  0, 11,  3,  8]
        , [  9, 14, 15,  5,  2,  8, 12,  3,  7,  0,  4, 10,  1, 13, 11,  6]
        , [  4,  3,  2, 12,  9,  5, 15, 10, 11, 14,  1,  7,  6,  0,  8, 13]
        ],
        [ [  4, 11,  2, 14, 15, 0,  8, 13,  3, 12, 9,  7,  5, 10, 6,  1]
        , [ 13,  0, 11,  7,  4, 9,  1, 10, 14,  3, 5, 12,  2, 15, 8,  6]
        , [  1,  4, 11, 13, 12, 3,  7, 14, 10, 15, 6,  8,  0,  5, 9,  2]
        , [  6, 11, 13,  8,  1, 4, 10,  7,  9,  5, 0, 15, 14,  2, 3, 12]
        ],
        [ [ 13,  2,  8,  4,  6, 15, 11,  1, 10,  9,  3, 14,  5,  0, 12,  7]
        , [  1, 15, 13,  8, 10,  3,  7,  4, 12,  5,  6, 11,  0, 14,  9,  2]
        , [  7, 11,  4,  1,  9, 12, 14,  2,  0,  6, 10, 13, 15,  3,  5,  8]
        , [  2,  1, 14,  7,  4, 10,  8, 13, 15, 12,  9,  0,  3,  5,  6, 11]
        ]];
        let i = ((block & 0x20) >> 4 | (block & 1)) as usize;
        let j = ((block & 0x1E) >> 1) as usize;
        unsafe { *TABLES.get_unchecked(box_id).get_unchecked(i).get_unchecked(j) }
        //TABLES[box_id][i][j]
}

#[cfg(test)]
mod bench {
    use test::{Bencher, black_box};

    use super::s;

    #[bench]
    fn bench_s(bencher: &mut Bencher) {
        bencher.iter(|| {
            let box_id = black_box(7);
            (0 .. 40_000_000).fold(0, |acc, block| acc + s(box_id, block))
        });
    }
}

我第一次使用[] 运行此基准测试时,它比使用get_unchecked() 的版本花费的时间更少(尽管平均而言get_unchecked() 版本似乎快了一点)。 我不确定它是否真的反映了我在现实生活中的基准(包括加密一个大文件),但它给出了一个想法。

我检查了程序集以确保编译器没有优化边界检查。

这里是get_unchecked()的版本:

0000000000009360 <_ZN3des7s_table17hbabdd9dee72331a5E>:
    9360:   48 89 f0                mov    %rsi,%rax
    9363:   48 c1 e8 04             shr    $0x4,%rax
    9367:   83 e0 02                and    $0x2,%eax
    936a:   89 f1                   mov    %esi,%ecx
    936c:   83 e1 01                and    $0x1,%ecx
    936f:   48 09 c1                or     %rax,%rcx
    9372:   48 c1 e7 09             shl    $0x9,%rdi
    9376:   48 8d 05 93 57 04 00    lea    0x45793(%rip),%rax        # 4eb10 <ref10404>
    937d:   48 01 f8                add    %rdi,%rax
    9380:   48 c1 e1 07             shl    $0x7,%rcx
    9384:   48 01 c1                add    %rax,%rcx
    9387:   83 e6 1e                and    $0x1e,%esi
    938a:   48 8b 04 b1             mov    (%rcx,%rsi,4),%rax
    938e:   c3                      retq   
    938f:   90                      nop

这是带有[]的版本:

0000000000009390 <_ZN3des7s_table17hbabdd9dee72331a5E>:
    9390:   50                      push   %rax
    9391:   48 89 f8                mov    %rdi,%rax
    9394:   48 83 f8 07             cmp    $0x7,%rax
    9398:   77 30                   ja     93ca <_ZN3des7s_table17hbabdd9dee72331a5E+0x3a>
    939a:   48 89 f1                mov    %rsi,%rcx
    939d:   48 c1 e9 04             shr    $0x4,%rcx
    93a1:   83 e1 02                and    $0x2,%ecx
    93a4:   89 f2                   mov    %esi,%edx
    93a6:   83 e2 01                and    $0x1,%edx
    93a9:   48 09 ca                or     %rcx,%rdx
    93ac:   48 c1 e0 09             shl    $0x9,%rax
    93b0:   48 8d 0d 99 57 04 00    lea    0x45799(%rip),%rcx        # 4eb50 <const10401>
    93b7:   48 01 c1                add    %rax,%rcx
    93ba:   48 c1 e2 07             shl    $0x7,%rdx
    93be:   48 01 ca                add    %rcx,%rdx
    93c1:   83 e6 1e                and    $0x1e,%esi
    93c4:   48 8b 04 b2             mov    (%rdx,%rsi,4),%rax
    93c8:   59                      pop    %rcx
    93c9:   c3                      retq   
    93ca:   48 8d 3d b7 a3 25 00    lea    0x25a3b7(%rip),%rdi        # 263788 <panic_bounds_check_loc10404>
    93d1:   ba 08 00 00 00          mov    $0x8,%edx
    93d6:   48 89 c6                mov    %rax,%rsi
    93d9:   e8 82 25 04 00          callq  4b960 <_ZN4core9panicking18panic_bounds_check17hb2d969c3cc11ed08E>
    93de:   66 90                   xchg   %ax,%ax

我们可以看到get_unchecked() 的版本更小,[] 的版本检查边界。

两个版本都在发布模式下编译。

为什么get_unchecked() 版本没有比这更快? 当get_unchecked()/[]被调用48亿次时,我认为它应该比[]版本快至少几秒钟。

编辑:我使用valgrind分析了代码。

带有[] 的版本显示带有数组索引的行的成本为10,而带有get_unchecked() 的版本显示的成本小于1(见下图)。 但是,该功能的成本(参见图像左侧)保持不变。这很奇怪。有人解释一下吗?

带有get_unchecked()的版本。

[]的版本

【问题讨论】:

  • 分支预测?您的分析器告诉您使用边界检查代码产生的 CPU 有多少错误预测?
  • 如果 [] 被调用 48 亿次,那么至少一条指令(用于边界检查)将被调用 48 亿次,对吧?所以即使 CPU 预测分支,我认为我们应该看到两个版本之间的显着差异。我错了吗?
  • 你正在做很多人都会做的事情——猜测,并要求其他人猜测。有no need to guess
  • @MikeDunlavey 我使用了分析器(valgrind)。几天来,我一直在提高我的 crate 的性能,毕竟我的速度提高了 30 倍。从[] 切换到get_unchecked() 使得这条特定线路的成本从10 降低到小于1。但是,该功能的总体成本保持不变。这对我来说很奇怪。
  • 我认为这是一个很好的问题,但它与 Rust 无关。 IMO 与 get_unchecked 没有区别,因为 CPU 不是瓶颈。

标签: performance assembly optimization rust


【解决方案1】:

我还没有学多少 Rust(还),但我想我仍然可以回答这个性能部分。

首先,您确定您的基准测试实际上是在运行该函数的独立版本吗?它可能会将函数内联到调用站点,其中box_id 是编译时常量。除了消除较小的调用/调用开销外,表索引计算 asm 将大大简化。此外,如果在编译时就知道没有超出边界检查,则可以省略边界检查。

如果有人展示了如何修改 OP 的示例以编译为实际的 asm on the Godbolt compiler explorer,我可以看看并说更多。将它按原样放在 Godbolt 上会产生一些编译为空 asm 输出的东西。我可能决定自己学习足够多的 Rust 来做这件事,但可能不会很快。


看看单机版的函数:

检查版中的额外说明只是前4条:

9390:   50                      push   %rax        # align the stack in case we make a function call (wasted work for the common case)
9391:   48 89 f8                mov    %rdi,%rax   # compiler is dumb, could have checked the value in %rdi
9394:   48 83 f8 07             cmp    $0x7,%rax
9398:   77 30                   ja     93ca <_ZN3des7s_table17hbabdd9dee72331a5E+0x3a>

最后的 pop %rcx 将 8 添加到 %rsp。

所以这是函数开始时的 4 微秒。 (Core2Duo 无法在 64 位模式下进行宏融合。不过,Core2Duo 可以在 32 位代码中对 cmp/ja 进行宏融合。(请参阅Agner Fog's microarch pdf)。如果编译器更智能,则只需 2 条额外指令/ uops 在通过函数的快速路径上总计(cmp/ja),另一个函数调用的堆栈对齐仅在实际进行调用的分支中完成。

您可能认为将这 4 个微指令问题作为函数的第一组会是一个问题,因为它会延迟 CPU 获取关键路径上的指令。但事实并非如此,因为显然您的代码在前端没有瓶颈。 (您没有显示在循环中调用它的代码的 asm)。因此,指令在实际执行的指令之前被发送到乱序内核。

可能当函数输入在 %rdi 中准备好时,调度程序已经有关键路径指令在等待它。如果基准测试实际上运行的是独立版本,则函数开头的 4 条额外指令实际上并没有延迟关键路径。因此,据推测,关键路径的瓶颈在于延迟,而不是吞吐量。 (一个调用的输出是否形成了下一次查找的索引?如果是这样,那将防止函数的多个调用同时针对不同的输入执行。L1 负载使用延迟在 Core 2 上为 3 个周期(根据Agner Fog's microarch pdf)。

不过,在根据输入计算表索引的指令中有相当多的指令级并行性。有几个mov 指令可以制作副本,然后指令对副本和原件做不同的事情。并且这两个 args 已经分开了。我认为在输入准备好和表索引准备好之间,可能有足够的并行性在很多时候并行运行 3 条指令。因此,如果在有另一个输入之前执行必须等待 3 个周期进行表查找,那就是运行额外微指令的时候了。 (不过,调度程序在先就绪的基础上运行 uops,而不是关键路径优先,因此您可能会期望它们有时会因资源冲突(从关键路径窃取执行端口)而延长关键路径。

TL:DR 如果一个调用的输出是下一个调用的输入,那么 L1 负载使用延迟仍然很容易成为瓶颈,而不是 uop 吞吐量。否则必须有一些其他的瓶颈,让 5 个额外的 uops 有时间运行而不会延迟“实际工作”。否则检查会在实际运行的代码中被优化掉。

【讨论】:

    猜你喜欢
    • 2015-05-26
    • 1970-01-01
    • 1970-01-01
    • 2015-07-16
    • 1970-01-01
    • 2010-12-09
    • 1970-01-01
    • 2017-11-19
    • 2013-06-05
    相关资源
    最近更新 更多