Skip to content

Latest commit

 

History

19 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Optimizing insertions in the middle of a collection

Note that all these arre just surface-level observations and I haven't dug deep enough (yet).

Loop unrolling

It seems like reducing the amount of branching in a loop gives the compiler a better chance to perform loop unrolling. Consider a loop that checks the value of an index prior to performing any other operation:

for (index, x) in xs.iter().enumerate() {
    if index != mid {
        if *x < pivot {
            left += x;
        } else {
            right += x;
        }
    }
}

The following code is functionally equivalent but is written in such a way that it is not necessary to check the value of the indexes during each loop. The compiler can do loop unrolling in this case:

for x in xs.iter().take(mid) {
    if *x < pivot {
        left += x;
    } else {
        right += x;
    }
}

for x in xs.iter().skip(mid + 1) {
    if *x < pivot {
        left += x;
    } else {
        right += x;
    }
}

Using iterators

This example already is unrolled by the compiler:

#[no_mangle]
pub fn sort_around_pivot(xs: &[i32]) -> i32 {
    let mid = xs.len() / 2;
    let pivot = xs[mid];

    let left: i32 = xs.iter().filter(|i| **i < pivot).sum();

    let right: i32 = xs
        .iter()
        .enumerate()
        .filter(|(index, i)| *index != mid && **i >= pivot)
        .map(|(_, i)| i)
        .sum();

    left + right
}

At a glance, it doesn't seem like this is much more optimized:

#[no_mangle]
pub fn sort_around_pivot(xs: &[i32]) -> i32 {
    let mid = xs.len() / 2;
    let pivot = xs[mid];

    let left: i32 = xs.iter().filter(|i| **i < pivot).sum();

    let right_till_mid: i32 = xs.iter().take(mid).filter(|i| **i >= pivot).sum();

    let right_from_mid: i32 = xs.iter().skip(mid + 1).filter(|i| **i >= pivot).sum();

    left + right_till_mid + right_from_mid
}

Since Compiler Explorer requires to work on non-mangled functions, and generics must be mangled, it is necessary to work with non-generic function. Furthermore, it is much easier to read and analyze code that is not operating on complicated types and, thus, it is easier to have a function that returns an i32 rather than a Vec<i32>.

The benchmark results show that it is better to use less iterators (i.e., left and right) rather than the implementation with right_till_mid and right_from_mid.

Using threads

The benchmarks show that whatever additional cost is associated with using additional threads is not worth it. The dissassembly of a simpler example shows that there is a significant amount of extra code whenever working with multiple threads:

use std::thread;

#[no_mangle]
pub fn sort_around_pivot(xs: &[i32]) -> (i32, i32) {
    let mid = xs.len() / 2;
    let pivot = xs[mid];

    let mut before_mid_left = 0;
    let mut before_mid_right = 0;
    let mut after_mid_left = 0;
    let mut after_mid_right = 0;

    let (before_mid, after_mid) = xs.split_at(mid);
    let after_mid = &after_mid[1..];

    thread::scope(|s| {
        s.spawn(|| {
            for x in before_mid {
                if *x < pivot {
                    before_mid_left += x;
                } else {
                    before_mid_right += x;
                }
            }
        });

        s.spawn(|| {
            for x in after_mid {
                if *x < pivot {
                    after_mid_left += x;
                } else {
                    after_mid_right += x;
                }
            }
        });
    });

    (
        before_mid_left + after_mid_left,
        before_mid_right + after_mid_right,
    )
}

About

Some of my notes on optimizing insertions in the middle of a collection

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages