You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Iteration performance for 'f' same-order arrays #749
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?)
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".
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
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.
@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);
}
self.layout is not reliable because Layout::None is 0 and it "eats" (&=) all other layouts.
self.parts may or may not contain an Indices type. I'm not sure how to verify.
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'?
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?
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.
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)
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)
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.
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.
Activity
bluss commentedon Nov 6, 2019
Please make this more specific with some example code, at least the stanzas that call Zip
nilgoyette commentedon Nov 6, 2019
Haha, wow, you're right, this is missing a lot of details! Here's my benchmark
The results looks like
Imo
Zip::fromand with all different-order, because they should be slower and they are.zip_indexed_ffshould not be so slow. Same-order 'c' is super fast and 'f' is super slow.zip_mut_with_ff[-]Zip::indexed performance for same-order arrays[/-][+]Iteration performance for 'f' same-order arrays[/+]nilgoyette commentedon Nov 7, 2019
I added some benchmarks.
The results looks like
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_ffto be "wrong".bluss commentedon Nov 10, 2019
Thanks for adding all that! Not sure what you mean about the
std_iter_benchmarks (those are interesting to have for completeness, though).nilgoyette commentedon Nov 11, 2019
IMO, we can ignore all different order (
fcandcf) 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 writec-c order iteration is quite fast.
Compared to c-c, f-f order iteration is abnormally slow
As a user, it "looks" like a bug.
jturner314 commentedon Nov 11, 2019
std_iter_ffis 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_ffis slow because: (1) theapply_core_stridedmethod is chosen instead ofapply_core_contiguousdue to the use of.indexed()(which has layoutLayout::none()) and (2)apply_core_stridedalways iterates in 'c' order.zip_mut_with_ffis slow becausezip_mut_with_same_shapefalls back tozip_mut_with_by_rowswhen the arrays are not contiguous and in 'c' order.zip_mut_with_by_rowsthen 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 fixzip_indexed_ffandzip_mut_with_ff. It would be relatively straightforward to improvezip_indexed_ffsomewhat by changing the innermost loop to be over the smallest-stride axis.zip_mut_with_same_shapecould 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.nilgoyette commentedon Nov 18, 2019
@jturner314 I tried optimizing
apply_core_stridedfor 'f' order by I'm stuck. I want to code something likeself.layoutis not reliable becauseLayout::Noneis 0 and it "eats" (&=) all other layouts.self.partsmay or may not contain anIndicestype. I'm not sure how to verify.self.partsto collect information because it's a tuple.So, how would you code
same_order_f? Or do you have a better idea to handleZip::Indexed 'f'?nilgoyette commentedon Dec 2, 2019
I tried giving a
layouttoIndicesso it knows the proper layout to return infn layout(&self). I thought this would fix the three problems presented above, and I'm pretty sure it does, but nowapply_core_contiguousis called instead ofapply_core_strided. After working on it for a while, I realized that it's a bad idea because faking thatIndicesis contiguous forces the programmer to code aoffset_to_idxfunction which is super slow.In brief, I'm stuck on the same problem: how to know in
apply_core_stridedif all parts (except Indices) have 'c' order, 'f' order or a random mix?bluss commentedon Apr 20, 2020
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
Indicessequence 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.
bluss commentedon Apr 21, 2020
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.)
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)bluss commentedon Apr 22, 2020
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.
Indicesis anNdProducerand 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.jturner314 commentedon Apr 22, 2020
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.nditeris 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 onnditer, I came across NumPy's C iterator interface, which provides similar functionality, in particular with theNPY_KEEPORDERoption.