Skip to content

Iteration performance for 'f' same-order arrays #749

Closed

Description

@nilgoyette

We were wondering if mixing 'c' and 'f' order was as bad as the documentation says, so I created a series of benchmarks, comparing different/same order and loop/iter.zip/Zip::from/Zip::indexed/etc. Please read my next comment for more information. (Maybe this issue should be in #469?)

Activity

  1. bluss commented on Nov 6, 2019

    @bluss
    Member

    Please make this more specific with some example code, at least the stanzas that call Zip

  2. nilgoyette commented on Nov 6, 2019

    @nilgoyette
    CollaboratorAuthor

    Haha, wow, you're right, this is missing a lot of details! Here's my benchmark

    #![feature(test)]
    extern crate test;
    use test::{black_box, Bencher};
    use ndarray::{Array3, ShapeBuilder, Zip};
    
    pub fn zip_copy(data: &Array3<f32>, out: &mut Array3<f32>) {
        Zip::from(data).and(out).apply(|&i, o| {
            *o = i;
        });
    }
    
    pub fn zip_indexed(data: &Array3<f32>, out: &mut Array3<f32>) {
        Zip::indexed(data).and(out).apply(|idx, &i, o| {
            *o = i;
        });
    }
    
    pub fn zip_mut_with(data: &Array3<f32>, out: &mut Array3<f32>) {
        out.zip_mut_with(&data, |o, &i| {
            *o = i;
        });
    }
    
    #[bench]
    fn zip_cf(b: &mut Bencher) {
        let data: Array3<f32> = Array3::zeros((137, 171, 151));
        let mut out = Array3::zeros(data.dim().f());
        b.iter(|| black_box(zip_copy(&data, &mut out)));
    }
    
    #[bench]
    fn zip_cc(b: &mut Bencher) {
        let data: Array3<f32> = Array3::zeros((137, 171, 151));
        let mut out = Array3::zeros(data.dim());
        b.iter(|| black_box(zip_copy(&data, &mut out)));
    }
    
    #[bench]
    fn zip_fc(b: &mut Bencher) {
        let data: Array3<f32> = Array3::zeros((137, 171, 151).f());
        let mut out = Array3::zeros(data.dim());
        b.iter(|| black_box(zip_copy(&data, &mut out)));
    }
    
    #[bench]
    fn zip_ff(b: &mut Bencher) {
        let data: Array3<f32> = Array3::zeros((137, 171, 151).f());
        let mut out = Array3::zeros(data.dim().f());
        b.iter(|| black_box(zip_copy(&data, &mut out)));
    }
    
    #[bench]
    fn zip_indexed_cf(b: &mut Bencher) {
        let data: Array3<f32> = Array3::zeros((137, 171, 151));
        let mut out = Array3::zeros(data.dim().f());
        b.iter(|| black_box(zip_indexed(&data, &mut out)));
    }
    
    #[bench]
    fn zip_indexed_cc(b: &mut Bencher) {
        let data: Array3<f32> = Array3::zeros((137, 171, 151));
        let mut out = Array3::zeros(data.dim());
        b.iter(|| black_box(zip_indexed(&data, &mut out)));
    }
    
    #[bench]
    fn zip_indexed_fc(b: &mut Bencher) {
        let data: Array3<f32> = Array3::zeros((137, 171, 151).f());
        let mut out = Array3::zeros(data.dim());
        b.iter(|| black_box(zip_indexed(&data, &mut out)));
    }
    
    #[bench]
    fn zip_indexed_ff(b: &mut Bencher) {
        let data: Array3<f32> = Array3::zeros((137, 171, 151).f());
        let mut out = Array3::zeros(data.dim().f());
        b.iter(|| black_box(zip_indexed(&data, &mut out)));
    }
    
    #[bench]
    fn zip_mut_with_cf(b: &mut Bencher) {
        let data: Array3<f32> = Array3::zeros((137, 171, 151));
        let mut out = Array3::zeros(data.dim().f());
        b.iter(|| black_box(zip_mut_with(&data, &mut out)));
    }
    
    #[bench]
    fn zip_mut_with_cc(b: &mut Bencher) {
        let data: Array3<f32> = Array3::zeros((137, 171, 151));
        let mut out = Array3::zeros(data.dim());
        b.iter(|| black_box(zip_mut_with(&data, &mut out)));
    }
    
    #[bench]
    fn zip_mut_with_fc(b: &mut Bencher) {
        let data: Array3<f32> = Array3::zeros((137, 171, 151).f());
        let mut out = Array3::zeros(data.dim());
        b.iter(|| black_box(zip_mut_with(&data, &mut out)));
    }
    
    #[bench]
    fn zip_mut_with_ff(b: &mut Bencher) {
        let data: Array3<f32> = Array3::zeros((137, 171, 151).f());
        let mut out = Array3::zeros(data.dim().f());
        b.iter(|| black_box(zip_mut_with(&data, &mut out)));
    }
    

    The results looks like

    test zip_cc          ... bench:   1,981,155 ns/iter (+/- 155,092)
    test zip_cf          ... bench:   8,597,290 ns/iter (+/- 161,647)
    test zip_fc          ... bench:   8,310,620 ns/iter (+/- 283,961)
    test zip_ff          ... bench:   1,922,292 ns/iter (+/- 57,304)
    test zip_indexed_cc  ... bench:   1,983,300 ns/iter (+/- 59,128)
    test zip_indexed_cf  ... bench:   8,618,135 ns/iter (+/- 196,232)
    test zip_indexed_fc  ... bench:   8,378,690 ns/iter (+/- 174,317)
    test zip_indexed_ff  ... bench:  13,410,210 ns/iter (+/- 294,625)  // !
    test zip_mut_with_cc ... bench:   1,927,552 ns/iter (+/- 118,898)
    test zip_mut_with_cf ... bench:   8,545,440 ns/iter (+/- 742,765)
    test zip_mut_with_fc ... bench:   8,257,935 ns/iter (+/- 241,691)
    test zip_mut_with_ff ... bench:   7,375,450 ns/iter (+/- 705,054)  // !
    

    Imo

    • There's no problem with Zip::from and with all different-order, because they should be slower and they are.
    • zip_indexed_ff should not be so slow. Same-order 'c' is super fast and 'f' is super slow.
    • Idem for zip_mut_with_ff
  3. changed the title [-]Zip::indexed performance for same-order arrays[/-] [+]Iteration performance for 'f' same-order arrays[/+] on Nov 7, 2019
  4. nilgoyette commented on Nov 7, 2019

    @nilgoyette
    CollaboratorAuthor

    I added some benchmarks.

    pub fn loop_xyz_copy(data: &Array3<f32>, out: &mut Array3<f32>) {
        let (depth, height, width) = data.dim();
        for x in 0..width {
            for y in 0..height {
                for z in 0..depth {
                    out[(z, y, x)] = data[(z, y, x)];
                }
            }
        }
    }
    
    pub fn loop_zyx_copy(data: &Array3<f32>, out: &mut Array3<f32>) {
        let (depth, height, width) = data.dim();
        for z in 0..depth {
            for y in 0..height {
                for x in 0..width {
                    out[(z, y, x)] = data[(z, y, x)];
                }
            }
        }
    }
    
    pub fn iter_zip_copy(data: &Array3<f32>, out: &mut Array3<f32>) {
        for (&i, o) in data.iter().zip(out.iter_mut()) {
            *o = i;
        }
    }
    
    #[bench]
    fn std_iter_cf(b: &mut Bencher) {
        let data: Array3<f32> = Array3::zeros((137, 171, 151));
        let mut out = Array3::zeros(data.dim().f());
        b.iter(|| black_box(iter_zip_copy(&data, &mut out)));
    }
    
    #[bench]
    fn std_iter_cc(b: &mut Bencher) {
        let data: Array3<f32> = Array3::zeros((137, 171, 151));
        let mut out = Array3::zeros(data.dim());
        b.iter(|| black_box(iter_zip_copy(&data, &mut out)));
    }
    
    #[bench]
    fn std_iter_fc(b: &mut Bencher) {
        let data: Array3<f32> = Array3::zeros((137, 171, 151).f());
        let mut out = Array3::zeros(data.dim());
        b.iter(|| black_box(iter_zip_copy(&data, &mut out)));
    }
    
    #[bench]
    fn std_iter_ff(b: &mut Bencher) {
        let data: Array3<f32> = Array3::zeros((137, 171, 151).f());
        let mut out = Array3::zeros(data.dim().f());
        b.iter(|| black_box(iter_zip_copy(&data, &mut out)));
    }
    
    #[bench]
    fn loops_xyz_cf(b: &mut Bencher) {
        let data: Array3<f32> = Array3::zeros((137, 171, 151));
        let mut out = Array3::zeros(data.dim().f());
        b.iter(|| black_box(loop_xyz_copy(&data, &mut out)));
    }
    
    #[bench]
    fn loops_xyz_cc(b: &mut Bencher) {
        let data: Array3<f32> = Array3::zeros((137, 171, 151));
        let mut out = Array3::zeros(data.dim());
        b.iter(|| black_box(loop_xyz_copy(&data, &mut out)));
    }
    
    #[bench]
    fn loops_xyz_fc(b: &mut Bencher) {
        let data: Array3<f32> = Array3::zeros((137, 171, 151).f());
        let mut out = Array3::zeros(data.dim());
        b.iter(|| black_box(loop_xyz_copy(&data, &mut out)));
    }
    
    #[bench]
    fn loops_xyz_ff(b: &mut Bencher) {
        let data: Array3<f32> = Array3::zeros((137, 171, 151).f());
        let mut out = Array3::zeros(data.dim().f());
        b.iter(|| black_box(loop_xyz_copy(&data, &mut out)));
    }
    
    #[bench]
    fn loops_zyx_cf(b: &mut Bencher) {
        let data: Array3<f32> = Array3::zeros((137, 171, 151));
        let mut out = Array3::zeros(data.dim().f());
        b.iter(|| black_box(loop_zyx_copy(&data, &mut out)));
    }
    
    #[bench]
    fn loops_zyx_cc(b: &mut Bencher) {
        let data: Array3<f32> = Array3::zeros((137, 171, 151));
        let mut out = Array3::zeros(data.dim());
        b.iter(|| black_box(loop_zyx_copy(&data, &mut out)));
    }
    
    #[bench]
    fn loops_zyx_fc(b: &mut Bencher) {
        let data: Array3<f32> = Array3::zeros((137, 171, 151).f());
        let mut out = Array3::zeros(data.dim());
        b.iter(|| black_box(loop_zyx_copy(&data, &mut out)));
    }
    
    #[bench]
    fn loops_zyx_ff(b: &mut Bencher) {
        let data: Array3<f32> = Array3::zeros((137, 171, 151).f());
        let mut out = Array3::zeros(data.dim().f());
        b.iter(|| black_box(loop_zyx_copy(&data, &mut out)));
    }
    

    The results looks like

    test loops_xyz_cc    ... bench:  20,090,750 ns/iter (+/- 1,103,683)
    test loops_xyz_cf    ... bench:  17,693,340 ns/iter (+/- 811,288)
    test loops_xyz_fc    ... bench:   9,883,610 ns/iter (+/- 1,189,556)
    test loops_xyz_ff    ... bench:   4,255,010 ns/iter (+/- 637,707)
    test loops_zyx_cc    ... bench:   4,239,425 ns/iter (+/- 208,714)
    test loops_zyx_cf    ... bench:   9,875,540 ns/iter (+/- 666,566)
    test loops_zyx_fc    ... bench:  17,769,500 ns/iter (+/- 717,710)
    test loops_zyx_ff    ... bench:  19,663,700 ns/iter (+/- 1,197,024)
    test std_iter_cc     ... bench:   2,018,025 ns/iter (+/- 328,120)
    test std_iter_cf     ... bench:   9,883,140 ns/iter (+/- 301,573)
    test std_iter_fc     ... bench:  18,426,710 ns/iter (+/- 1,984,603)
    test std_iter_ff     ... bench:  25,910,370 ns/iter (+/- 1,952,409)
    

    I'm not really using those methods to iterate, but I think they should appear in this issue. The loop results are not surprising. When they are slow, it's because I'm doing something wrong. However, the std.iter are the worse results I got and I don't consider std_iter_ff to be "wrong".

  5. bluss commented on Nov 10, 2019

    @bluss
    Member

    Thanks for adding all that! Not sure what you mean about the std_iter_ benchmarks (those are interesting to have for completeness, though).

  6. nilgoyette commented on Nov 11, 2019

    @nilgoyette
    CollaboratorAuthor

    IMO, we can ignore all different order (fc and cf) benchmarks. They are indeed slower but they represent a bad usage of ndarray and the documentation clearly states to not mix different order arrays. If I had to do a tl;rd of this issue, I would simply write


    c-c order iteration is quite fast.

    test zip_cc          ... bench:   1,981,155 ns/iter (+/- 155,092)
    test zip_indexed_cc  ... bench:   1,983,300 ns/iter (+/- 59,128)
    test zip_mut_with_cc ... bench:   1,927,552 ns/iter (+/- 118,898)
    test std_iter_cc     ... bench:   2,018,025 ns/iter (+/- 328,120)
    test loops_zyx_cc    ... bench:   4,239,425 ns/iter (+/- 208,714)
    

    Compared to c-c, f-f order iteration is abnormally slow

    test zip_ff          ... bench:   1,922,292 ns/iter (+/- 57,304)     As fast
    test zip_indexed_ff  ... bench:  13,410,210 ns/iter (+/- 294,625)    ~7x slower
    test zip_mut_with_ff ... bench:   7,375,450 ns/iter (+/- 705,054)    ~4x slower
    test std_iter_ff     ... bench:  25,910,370 ns/iter (+/- 1,952,409)  ~13x slower
    test loops_xyz_ff    ... bench:   4,255,010 ns/iter (+/- 637,707)    As fast
    

    As a user, it "looks" like a bug.

  7. jturner314 commented on Nov 11, 2019

    @jturner314
    Member

    std_iter_ff is slow, as expected, since the iterators produced by .iter() and .iter_mut() always iterate in 'c' order. The iteration order is mentioned in the docs, but maybe we should add a note mentioning the performance implication.

    zip_indexed_ff is slow because: (1) the apply_core_strided method is chosen instead of apply_core_contiguous due to the use of .indexed() (which has layout Layout::none()) and (2) apply_core_strided always iterates in 'c' order.

    zip_mut_with_ff is slow because zip_mut_with_same_shape falls back to zip_mut_with_by_rows when the arrays are not contiguous and in 'c' order. zip_mut_with_by_rows then iterates such that the innermost loop is over the last axis (which has the largest stride for 'f'-order arrays).

    We can'd do much about std_iter_ff, but we can fix zip_indexed_ff and zip_mut_with_ff. It would be relatively straightforward to improve zip_indexed_ff somewhat by changing the innermost loop to be over the smallest-stride axis. zip_mut_with_same_shape could be improved by checking if both arrays have the same layout, and if so, using .as_slice_memory_order/_mut() instead of .as_slice/_mut().

    Personally, my long-term plan is to replace these things with nditer, but it'll probably be a couple of months before I have the time to finish testing and release it.

  8. nilgoyette commented on Nov 18, 2019

    @nilgoyette
    CollaboratorAuthor

    @jturner314 I tried optimizing apply_core_strided for 'f' order by I'm stuck. I want to code something like

    let same_order_f: bool = true if all self.parts (except Indices) are in 'f' order;
    let unroll_axis = if same_order_f { 0 } else { n - 1 };
    ... vars and while loop
    if same_order_f {
        index_ = self.dimension.next_for_f(index);
        //          Must modify next_for_f to look like next_for
    } else {
        index_ = self.dimension.next_for(index);
    }
    
    1. self.layout is not reliable because Layout::None is 0 and it "eats" (&=) all other layouts.
    2. self.parts may or may not contain an Indices type. I'm not sure how to verify.
    3. I can't loop on self.parts to collect information because it's a tuple.

    So, how would you code same_order_f? Or do you have a better idea to handle Zip::Indexed 'f'?

  9. nilgoyette commented on Dec 2, 2019

    @nilgoyette
    CollaboratorAuthor

    I tried giving a layout to Indices so it knows the proper layout to return in fn layout(&self). I thought this would fix the three problems presented above, and I'm pretty sure it does, but now apply_core_contiguous is called instead of apply_core_strided. After working on it for a while, I realized that it's a bad idea because faking that Indices is contiguous forces the programmer to code a offset_to_idx function which is super slow.

    In brief, I'm stuck on the same problem: how to know in apply_core_strided if all parts (except Indices) have 'c' order, 'f' order or a random mix?

  10. bluss commented on Apr 20, 2020

    @bluss
    Member

    Well, the layout value is None by how it's constructed, so sounds like the construction of the Zip (in the general sense) needs to be studied to solve this (take a step out).

    I'm curious about what scheme you'd use for computing indices quickly? Maybe the current scheme is quite ok(?). I'm sorry I don't have more to offer, I'd just steal the problem away from you anyway 🧀 and don't want to do that 😉 (Edit: I failed)

    Simple-level fix: Pick a c-biased or f-biased index producer when starting, is that possible? More complex: Rethink the whole NdProducer trait so that it can support this better.

    Edit: The fundamental trick of the contiguous layouts (c- or f-contig) is that we can ignore axes and dimensions. We just treat all inputs as linear arrays and increment one variable as a counter across the whole thing. I don't see a way to look at an Indices sequence and do that, we must "roll over" the increment on the fastest-changing index to the next-fastest-changing one and so on.

    Seems like it would be good to look around for prior art.

    I'll admit that I'm not very interested in indexed iteration, because it will be slow. :)

    The loops benchmarks would be interesting with unchecked indexing, to see if they match zip in performance.

  11. bluss commented on Apr 21, 2020

    @bluss
    Member

    This is a quick hack, but this is what I mean by construction. A quick hack to keep track of c- or f- "tendency" when building the Zip and picking inner loop depending on this, yields equitable performance (I think the results are noisy). (We also need to make new benchmarks where the index is actually used, and see how that affects this.)

    running 12 tests
    test zip_cc          ... bench:     574,941 ns/iter (+/- 203,820)
    test zip_cf          ... bench:   2,719,307 ns/iter (+/- 1,713,805)
    test zip_fc          ... bench:   2,196,897 ns/iter (+/- 1,448,511)
    test zip_ff          ... bench:     539,996 ns/iter (+/- 333,392)
    test zip_indexed_cc  ... bench:     881,276 ns/iter (+/- 279,357)
    test zip_indexed_cf  ... bench:   3,027,802 ns/iter (+/- 2,340,005)
    test zip_indexed_fc  ... bench:   2,057,937 ns/iter (+/- 761,223)
    test zip_indexed_ff  ... bench:     673,751 ns/iter (+/- 243,699)
    test zip_mut_with_cc ... bench:     472,752 ns/iter (+/- 152,175)
    test zip_mut_with_cf ... bench:   1,613,787 ns/iter (+/- 677,141)
    test zip_mut_with_fc ... bench:   1,187,558 ns/iter (+/- 497,037)
    test zip_mut_with_ff ... bench:     552,803 ns/iter (+/- 280,990)

    The code is in the branch https://github.com/rust-ndarray/ndarray/pull/new/zip-strided-for-c-and-f

    @nilgoyette

    It could be included with just a bit of cleanup, even if we of course want a nicer and smarter inner strided loop than this. If we show that it has improvements for other mixes of arrays, that's much better (for example, not with indexing but with sliced arrays).

    Preferably, it could pick up an f-preference even if none of the inputs are f-contiguous but "leaning f" (which means, Axis(0) is the shortest stride axis.) (Edit: Added even quicker hack for that, in the branch)

  12. bluss commented on Apr 22, 2020

    @bluss
    Member

    I have been thoroughly nerd-sniped, and will post updates that improve this much in the way that @nilgoyette has requested. I'll try to be brief so that it has higher signal to noise ratio now.

    • Indices is an NdProducer and as a producer it is not biased versus either C/F layout in anyway, it is equally (in) efficient at producing indices for either layout.
      • It uses a refined (read: Hard to understand) implementation to accomplish this
    • What we will improve is how Zip selects how to iterate based on all the inputs, so that the Indices producer does not by itself cause a fallback to the default C layout
    • As we said before, we don't know a trick for "contiguous-like" iteration of an index producer. This shortcut, where everything is 1D, is not available for multidimensional indices.
    • We'll improve so that we also recognize sliced arrays.
    • All improvements impact parallelization too, because we will know better along which axis to split the Zip to preserve locality
    • We're still using an "C-" or "F-" preferred model, either the first or last axis is the tightest, this handles the common cases.
  13. jturner314 commented on Apr 22, 2020

    @jturner314
    Member

    Right, this is similar to, but somewhat simpler than, the approach I'm working on in nditer. Basically, choose the optimal iteration order and merge axes when possible based on the memory layouts of the inputs. This can produce substantial performance gains. Indexed iteration prevents axes from being merged, but the iteration order can still be modified. nditer is mostly complete; I primarily need to improve the test coverage. I just haven't had much time recently to work on open source projects. Fwiw, while working on nditer, I came across NumPy's C iterator interface, which provides similar functionality, in particular with the NPY_KEEPORDER option.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions