Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve perf of Enumerable.Order{Descending} for primitives #76733

Merged
merged 2 commits into from
Oct 10, 2022

Conversation

stephentoub
Copy link
Member

For non-floating point primitive types, stable sorts are indistinguishable from unstable sorts. They could be distinguishable if there are custom keys associated with each item, but Order{Descending} doesn't have separate keys from the elements themselves. As such, we can avoid all of the overhead associated with the int[] map Order{Descending}By creates as part of implementing the stable sort, avoid always specifying a comparer, etc. This PR does so for Order{Descending}(comparer) followed by ToArray, ToList, and foreach/GetEnumerator.

[Params(10, 1_000, 1_000_000)]
public int Size { get; set; }

[Params(false, true)]
public bool CustomComparer { get; set; }

private IEnumerable<int> _source;
private IComparer<int> _comparer;

[GlobalSetup]
public void Setup()
{
    _source = Enumerable.Range(0, Size).Reverse();
    _comparer = CustomComparer ? Comparer<int>.Create((a, b) => a.CompareTo(b)) : null;
}

[Benchmark]
public int[] OrderToArray() => _source.Order(_comparer).ToArray();

[Benchmark]
public List<int> OrderToList() => _source.Order(_comparer).ToList();

[Benchmark]
public int[] OrderDescendingToArray() => _source.OrderDescending(_comparer).ToArray();

[Benchmark]
public List<int> OrderDescendingToList() => _source.OrderDescending(_comparer).ToList();
Method Toolchain Size CustomComparer Mean Ratio Allocated Alloc Ratio
OrderToArray \main\corerun.exe 10 False 278.17 ns 1.00 368 B 1.00
OrderToArray \pr\corerun.exe 10 False 92.35 ns 0.33 104 B 0.28
OrderToList \main\corerun.exe 10 False 319.36 ns 1.00 400 B 1.00
OrderToList \pr\corerun.exe 10 False 111.68 ns 0.35 136 B 0.34
OrderDescendingToArray \main\corerun.exe 10 False 141.88 ns 1.00 368 B 1.00
OrderDescendingToArray \pr\corerun.exe 10 False 78.19 ns 0.55 104 B 0.28
OrderDescendingToList \main\corerun.exe 10 False 153.89 ns 1.00 400 B 1.00
OrderDescendingToList \pr\corerun.exe 10 False 102.72 ns 0.67 136 B 0.34
OrderToArray \main\corerun.exe 10 True 420.89 ns 1.00 368 B 1.00
OrderToArray \pr\corerun.exe 10 True 292.21 ns 0.69 168 B 0.46
OrderToList \main\corerun.exe 10 True 438.26 ns 1.00 400 B 1.00
OrderToList \pr\corerun.exe 10 True 298.27 ns 0.68 200 B 0.50
OrderDescendingToArray \main\corerun.exe 10 True 175.02 ns 1.00 368 B 1.00
OrderDescendingToArray \pr\corerun.exe 10 True 114.31 ns 0.68 192 B 0.52
OrderDescendingToList \main\corerun.exe 10 True 192.11 ns 1.00 400 B 1.00
OrderDescendingToList \pr\corerun.exe 10 True 132.25 ns 0.69 224 B 0.56
OrderToArray \main\corerun.exe 1000 False 37,076.72 ns 1.00 12248 B 1.00
OrderToArray \pr\corerun.exe 1000 False 8,460.32 ns 0.21 4064 B 0.33
OrderToList \main\corerun.exe 1000 False 39,205.11 ns 1.00 12280 B 1.00
OrderToList \pr\corerun.exe 1000 False 9,620.57 ns 0.25 4096 B 0.33
OrderDescendingToArray \main\corerun.exe 1000 False 22,122.96 ns 1.00 12248 B 1.00
OrderDescendingToArray \pr\corerun.exe 1000 False 21,061.94 ns 0.95 4064 B 0.33
OrderDescendingToList \main\corerun.exe 1000 False 25,929.92 ns 1.00 12280 B 1.00
OrderDescendingToList \pr\corerun.exe 1000 False 21,786.70 ns 0.84 4096 B 0.33
OrderToArray \main\corerun.exe 1000 True 78,531.66 ns 1.00 12248 B 1.00
OrderToArray \pr\corerun.exe 1000 True 46,769.75 ns 0.59 4128 B 0.34
OrderToList \main\corerun.exe 1000 True 78,907.46 ns 1.00 12280 B 1.00
OrderToList \pr\corerun.exe 1000 True 48,665.61 ns 0.62 4160 B 0.34
OrderDescendingToArray \main\corerun.exe 1000 True 48,244.70 ns 1.00 12248 B 1.00
OrderDescendingToArray \pr\corerun.exe 1000 True 35,226.95 ns 0.73 4152 B 0.34
OrderDescendingToList \main\corerun.exe 1000 True 47,008.31 ns 1.00 12280 B 1.00
OrderDescendingToList \pr\corerun.exe 1000 True 35,854.03 ns 0.76 4184 B 0.34
OrderToArray \main\corerun.exe 1000000 False 106,464,660.88 ns 1.00 12000356 B 1.00
OrderToArray \pr\corerun.exe 1000000 False 17,732,985.94 ns 0.17 4000094 B 0.33
OrderToList \main\corerun.exe 1000000 False 85,958,815.56 ns 1.00 12000388 B 1.00
OrderToList \pr\corerun.exe 1000000 False 18,666,339.84 ns 0.22 4000126 B 0.33
OrderDescendingToArray \main\corerun.exe 1000000 False 54,612,728.42 ns 1.00 12000661 B 1.00
OrderDescendingToArray \pr\corerun.exe 1000000 False 48,403,715.76 ns 0.88 4000095 B 0.33
OrderDescendingToList \main\corerun.exe 1000000 False 64,468,350.83 ns 1.00 12000361 B 1.00
OrderDescendingToList \pr\corerun.exe 1000000 False 48,894,085.31 ns 0.76 4000127 B 0.33
OrderToArray \main\corerun.exe 1000000 True 192,562,314.29 ns 1.00 12000360 B 1.00
OrderToArray \pr\corerun.exe 1000000 True 114,808,988.57 ns 0.60 4000195 B 0.33
OrderToList \main\corerun.exe 1000000 True 194,070,950.00 ns 1.00 12000392 B 1.00
OrderToList \pr\corerun.exe 1000000 True 120,640,043.40 ns 0.62 4000227 B 0.33
OrderDescendingToArray \main\corerun.exe 1000000 True 114,865,036.00 ns 1.00 12001080 B 1.00
OrderDescendingToArray \pr\corerun.exe 1000000 True 81,669,459.18 ns 0.71 4000200 B 0.33
OrderDescendingToList \main\corerun.exe 1000000 True 110,513,381.48 ns 1.00 12000410 B 1.00
OrderDescendingToList \pr\corerun.exe 1000000 True 88,667,991.07 ns 0.80 4000240 B 0.33

For non-floating point primitive types, stable sorts are indistinguishable from unstable sorts.  They could be distinguishable if there are custom keys associated with each item, but Order{Descending} doesn't have separate keys from the elements themselves.  As such, we can avoid all of the overhead associated with the int[] map Order{Descending}By creates as part of implementing the stable sort, avoid always specifying a comparer, etc.  This PR does so for Order{Descending}(comparer) followed by ToArray, ToList, and foreach/GetEnumerator.
@stephentoub stephentoub added area-System.Linq tenet-performance Performance related issue labels Oct 7, 2022
@stephentoub stephentoub added this to the 8.0.0 milestone Oct 7, 2022
@ghost ghost assigned stephentoub Oct 7, 2022
@ghost
Copy link

ghost commented Oct 7, 2022

Tagging subscribers to this area: @dotnet/area-system-linq
See info in area-owners.md if you want to be subscribed.

Issue Details

For non-floating point primitive types, stable sorts are indistinguishable from unstable sorts. They could be distinguishable if there are custom keys associated with each item, but Order{Descending} doesn't have separate keys from the elements themselves. As such, we can avoid all of the overhead associated with the int[] map Order{Descending}By creates as part of implementing the stable sort, avoid always specifying a comparer, etc. This PR does so for Order{Descending}(comparer) followed by ToArray, ToList, and foreach/GetEnumerator.

[Params(10, 1_000, 1_000_000)]
public int Size { get; set; }

[Params(false, true)]
public bool CustomComparer { get; set; }

private IEnumerable<int> _source;
private IComparer<int> _comparer;

[GlobalSetup]
public void Setup()
{
    _source = Enumerable.Range(0, Size).Reverse();
    _comparer = CustomComparer ? Comparer<int>.Create((a, b) => a.CompareTo(b)) : null;
}

[Benchmark]
public int[] OrderToArray() => _source.Order(_comparer).ToArray();

[Benchmark]
public List<int> OrderToList() => _source.Order(_comparer).ToList();

[Benchmark]
public int[] OrderDescendingToArray() => _source.OrderDescending(_comparer).ToArray();

[Benchmark]
public List<int> OrderDescendingToList() => _source.OrderDescending(_comparer).ToList();
Method Toolchain Size CustomComparer Mean Ratio Allocated Alloc Ratio
OrderToArray \main\corerun.exe 10 False 278.17 ns 1.00 368 B 1.00
OrderToArray \pr\corerun.exe 10 False 92.35 ns 0.33 104 B 0.28
OrderToList \main\corerun.exe 10 False 319.36 ns 1.00 400 B 1.00
OrderToList \pr\corerun.exe 10 False 111.68 ns 0.35 136 B 0.34
OrderDescendingToArray \main\corerun.exe 10 False 141.88 ns 1.00 368 B 1.00
OrderDescendingToArray \pr\corerun.exe 10 False 78.19 ns 0.55 104 B 0.28
OrderDescendingToList \main\corerun.exe 10 False 153.89 ns 1.00 400 B 1.00
OrderDescendingToList \pr\corerun.exe 10 False 102.72 ns 0.67 136 B 0.34
OrderToArray \main\corerun.exe 10 True 420.89 ns 1.00 368 B 1.00
OrderToArray \pr\corerun.exe 10 True 292.21 ns 0.69 168 B 0.46
OrderToList \main\corerun.exe 10 True 438.26 ns 1.00 400 B 1.00
OrderToList \pr\corerun.exe 10 True 298.27 ns 0.68 200 B 0.50
OrderDescendingToArray \main\corerun.exe 10 True 175.02 ns 1.00 368 B 1.00
OrderDescendingToArray \pr\corerun.exe 10 True 114.31 ns 0.68 192 B 0.52
OrderDescendingToList \main\corerun.exe 10 True 192.11 ns 1.00 400 B 1.00
OrderDescendingToList \pr\corerun.exe 10 True 132.25 ns 0.69 224 B 0.56
OrderToArray \main\corerun.exe 1000 False 37,076.72 ns 1.00 12248 B 1.00
OrderToArray \pr\corerun.exe 1000 False 8,460.32 ns 0.21 4064 B 0.33
OrderToList \main\corerun.exe 1000 False 39,205.11 ns 1.00 12280 B 1.00
OrderToList \pr\corerun.exe 1000 False 9,620.57 ns 0.25 4096 B 0.33
OrderDescendingToArray \main\corerun.exe 1000 False 22,122.96 ns 1.00 12248 B 1.00
OrderDescendingToArray \pr\corerun.exe 1000 False 21,061.94 ns 0.95 4064 B 0.33
OrderDescendingToList \main\corerun.exe 1000 False 25,929.92 ns 1.00 12280 B 1.00
OrderDescendingToList \pr\corerun.exe 1000 False 21,786.70 ns 0.84 4096 B 0.33
OrderToArray \main\corerun.exe 1000 True 78,531.66 ns 1.00 12248 B 1.00
OrderToArray \pr\corerun.exe 1000 True 46,769.75 ns 0.59 4128 B 0.34
OrderToList \main\corerun.exe 1000 True 78,907.46 ns 1.00 12280 B 1.00
OrderToList \pr\corerun.exe 1000 True 48,665.61 ns 0.62 4160 B 0.34
OrderDescendingToArray \main\corerun.exe 1000 True 48,244.70 ns 1.00 12248 B 1.00
OrderDescendingToArray \pr\corerun.exe 1000 True 35,226.95 ns 0.73 4152 B 0.34
OrderDescendingToList \main\corerun.exe 1000 True 47,008.31 ns 1.00 12280 B 1.00
OrderDescendingToList \pr\corerun.exe 1000 True 35,854.03 ns 0.76 4184 B 0.34
OrderToArray \main\corerun.exe 1000000 False 106,464,660.88 ns 1.00 12000356 B 1.00
OrderToArray \pr\corerun.exe 1000000 False 17,732,985.94 ns 0.17 4000094 B 0.33
OrderToList \main\corerun.exe 1000000 False 85,958,815.56 ns 1.00 12000388 B 1.00
OrderToList \pr\corerun.exe 1000000 False 18,666,339.84 ns 0.22 4000126 B 0.33
OrderDescendingToArray \main\corerun.exe 1000000 False 54,612,728.42 ns 1.00 12000661 B 1.00
OrderDescendingToArray \pr\corerun.exe 1000000 False 48,403,715.76 ns 0.88 4000095 B 0.33
OrderDescendingToList \main\corerun.exe 1000000 False 64,468,350.83 ns 1.00 12000361 B 1.00
OrderDescendingToList \pr\corerun.exe 1000000 False 48,894,085.31 ns 0.76 4000127 B 0.33
OrderToArray \main\corerun.exe 1000000 True 192,562,314.29 ns 1.00 12000360 B 1.00
OrderToArray \pr\corerun.exe 1000000 True 114,808,988.57 ns 0.60 4000195 B 0.33
OrderToList \main\corerun.exe 1000000 True 194,070,950.00 ns 1.00 12000392 B 1.00
OrderToList \pr\corerun.exe 1000000 True 120,640,043.40 ns 0.62 4000227 B 0.33
OrderDescendingToArray \main\corerun.exe 1000000 True 114,865,036.00 ns 1.00 12001080 B 1.00
OrderDescendingToArray \pr\corerun.exe 1000000 True 81,669,459.18 ns 0.71 4000200 B 0.33
OrderDescendingToList \main\corerun.exe 1000000 True 110,513,381.48 ns 1.00 12000410 B 1.00
OrderDescendingToList \pr\corerun.exe 1000000 True 88,667,991.07 ns 0.80 4000240 B 0.33
Author: stephentoub
Assignees: -
Labels:

area-System.Linq, tenet-performance

Milestone: 8.0.0

@GSPP
Copy link

GSPP commented Oct 9, 2022

Hm, what if the custom comparer ignores some bits? Then, even integers become distinguishable. In the extreme, the comparer could compare just the lowest one bit. This effectively encodes key and value in the integer together.

@stephentoub
Copy link
Member Author

stephentoub commented Oct 9, 2022

what if the custom comparer ignores some bits?

What's an example where that would observably impact the resulting ordering, from an unstable vs stable perspective?

@GSPP
Copy link

GSPP commented Oct 9, 2022

I'll contrive a radically simple one. Let's say, the comparer always returns zero. Then:

Input: [0, 1]
Possible unstable output: [1, 0]

Or am I confused?

@stephentoub
Copy link
Member Author

Ah, that's fair. I was thinking of it from the perspective of the comparer saying that two identical values weren't, but it's true that if a comparer said that two non-identical values were the same that could result in observable differences in a stable vs unstable ordering.

@stephentoub stephentoub merged commit f156fb9 into dotnet:main Oct 10, 2022
@stephentoub stephentoub deleted the orderperf branch October 10, 2022 16:56
@ghost ghost locked as resolved and limited conversation to collaborators Nov 9, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-System.Linq tenet-performance Performance related issue
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants