首页 文章

将性能关键循环从C转换为Rust

提问于
浏览
1

我正在尝试将一些旧的C代码重写为Rust - 我是新手 . 我遇到的一个反复出现的问题是C代码有很多这样的循环:

for (i = startIndex; i < asize; i++)   
{
    if (firstEdge < 0 && condLeft(i))
    {
        firstEdge = i;
    }

    RightIndex = asize-1-i;
    if (firstEdgeRight < 0 && condRight(RightIndex))
    {
        firstEdgeRight = RightIndex;
    }

    // Found both edges
    if (firstEdge >= 0 && firstEdgeRight >= 0) {
        break;  
    }
}

你会如何以高效的方式将其转化为Rust?我的问题是,虽然我可能得到我想要的功能,但我不确定如何获得(大致)相同的速度 .

这部分代码是我们代码中的主要瓶颈(至少这部分代码),并且在翻译时它希望保留以下属性 .

  • 循环应该尽快破坏,因为 asize 可能非常大 .

  • firstEdgefirstEdgeRight 大致同时发现 . 因此,只有一个循环而不是两个循环是一件好事 - 为了避免再次从头开始搜索(即使我认为这个解决方案杀死了预取器(但我甚至有一个预取器)) .

虽然性能很重要,但可读性当然更为重要:)

EDIT 好的,这是我可能的Rust实现( cond_right()cond_left() 被遗漏了) . 我想到的是:

  • 这是否是其他人如果必须从头开始实施的话呢?

  • 我真的需要制作 first_edgefirst_edge_right 可变吗?它们在我的实现中,但我感觉不对,因为它们只被分配一次 .

let mut first_edge = -1;
let mut first_edge_right = -1;

// Find first edge

let start_index = 300; // or something
let asize = 10000000;

for i in start_index..asize {
    if first_edge < 0 && cond_left(i) {
        first_edge = i;
    }

    let right_index = asize - i -1;
    if first_edge_right < 0 && cond_right(right_index) {
        first_edge_right = right_index;
    }

    if (first_edge >= 0 && first_edge_right >= 0) {
        break;
    }
}

3 回答

  • 2

    你需要做好选择的准备:

    • 您如何以高效的方式将其转化为Rust?

    • 虽然表现很重要,但可读性当然更为重要

    哪个对你来说更重要?这是我编写代码的方式,假设我已经理解了你的要求:

    fn left_condition(i: usize) -> bool {
        i > 1000
    }
    
    fn right_condition(i: usize) -> bool {
        i % 73 == 0
    }
    
    fn main() {
        let start_index = 300;
        let asize = 10000000;
    
        let left = (start_index..asize).position(left_condition);
        let right = (start_index..asize).rev().position(right_condition);
    
        println!("{:?}, {:?}", left, right);
    }
    

    我们从左到右迭代一次,从右到左迭代一次 . 我的直觉告诉我,这将为代码提供简单的分支预测,以线性方式访问内存,这两者都应该是可优化的 .

    但是,变量名称 asize 让我暂停 . 它当然听起来像是"array size"的缩写 . 如果是这种情况,那么我会100%建议使用切片而不是数组索引 . 为什么?因为数组访问( foo[0] )通常具有边界检查的开销 . 我会用切片写一些东西:

    let data = vec![0; 10_000_000];
    let start_index = 300;
    
    let slice = &data[start_index..];
    
    let left = slice.iter().position(|&i| left_condition(i));
    let right = slice.iter().rev().position(|&i| right_condition(i));
    

    但是,对于您的问题,只有一个可能的真实答案:

    使用分析器

    使用分析器

    使用分析器

    使用分析器

    只知道您的实际数据,条件的实际实现,您正在运行的其余代码等,您才能真正了解某些事情的速度 .

    因此,只有一个循环而不是两个循环是一件好事 - 为了避免再次从头开始搜索

    这对我来说不直观,所以我希望看到支持声明的分析结果 .

  • 5

    cond_leftcond_right 对于回答性能问题非常重要 . 例如,用迭代器帮助替换索引吗?不知道 cond_left 做什么就不会告诉 .

    另一个问题是 first_edgefirst_edge_right 是可变的 . 提议的RFC allowing loops to return a value可能是解决这个问题的一种优雅方式 . 现在你可以使用闭包来模拟循环返回:

    let (_first_edge, _first_edge_right): (i32, i32) = (||{
        let (mut first_edge, mut first_edge_right) = (None, None);
        // ...
        return (first_edge.unwrap(), first_edge_right.unwrap());
    })();
    

    In Playground) .

    None 替换 -1 可能会使变量变大 . 见Can I use the "null pointer optimization" for my own non-pointer types? .

    将此循环拆分为两个循环,一个获取 first_edge ,另一个检查剩余范围以获取 first_edge_right 似乎是正确的事情,但CPU分支预测可能会最小化影响 .

  • 0

    由于同时保持双方的关注至关重要,我认为没有一种简单的方法可以避免变异变量 .

    可以提高可读性的一件事是使用option而不是负数 . 否则代码很好 .

    (你可能做的另一件事是当索引在中间相遇时打破循环,如果这意味着你的问题没有解决方案,但那不是Rust特有的 . )

相关问题