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

Refactor and document sorting dispatch #47383

Merged
merged 36 commits into from
Dec 3, 2022
Merged

Refactor and document sorting dispatch #47383

merged 36 commits into from
Dec 3, 2022

Conversation

LilithHafner
Copy link
Member

@LilithHafner LilithHafner commented Oct 29, 2022

Before, sorting dispatch used an ad-hoc system. I won't describe it here.

In this PR, sorting dispatch is specified by a nested chain of algorithms. For example, MissingOptimization(BoolOptimization(Small{10}(IEEEFloatOptimization(QuickSort)))) will attempt optimizations to handle eltypes that are unions with Missing and those that are Bool. Then it will dispatch to a small algorithm for vectors shorter than 10 elements, perform an optimization for eltypes that are IEEFloats, and finally, perform QuickSort.

This is a major refactoring, and part of a larger plan specified here. It should make future development more pleasant, improve the interactions with third party sorting libraries (e.g. SortingAlgorithms.jl), and facilitate future removal of many of the optimizations from base to put them in a stdlib.

For the most part, I attempted to limit observable changes to dispatch but I did separate missing optimizations from IEEE floating point optimizations. I kept a fair bit of code re-use, but they are fundamentally separate optimizations, and separating them closes #27781.

Further, I made the missing optimization call downstream algorithms with an abstract vector with eltype that does not include Missing. This allows that optimization to be combined with other optimizations like that for Bools and the UIntMappable suite.

One big difference between sort! and the _sort! function that this PR defines is that _sort! returns a scratch space (if any) rather than the sorted vector. Right now, that features is only lightly used, but it makes it possible to re-use scratch space allocated within a sorting method without pre-allocating beforehand. This is nice because it is often not obvious whether a sorting method will need scratch space at all. This fixes #47538, though it is likely not the only possible way to fix that issue.

I ran several benchmarks, many of which report runtime improvements.

Benchmark results EDITED to keep up to date with the PR

# mixed eltype with by order
julia> @btime sort(x; by=x -> x isa Symbol ? (0, x) : (1, x)) setup=(x=[rand() < .5 ? randstring() : Symbol(randstring()) for _ in 1:100]);
  31.586 μs (6 allocations: 1.88 KiB)  =>  30.311 μs (6 allocations: 1.88 KiB)

# reverse sorting Vector{Missing} (was 2ns in a previous version of this PR)
julia> @btime sort!(x, rev=true) setup=(x=fill(missing, 100));
  161.491 ns (3 allocations: 144 bytes)  =>  85.620 ns (0 allocations: 0 bytes)

# very small is still fast
julia> @btime sort(x) setup=(x=rand(2));
  56.484 ns (1 allocation: 80 bytes)  =>  52.098 ns (1 allocation: 80 bytes)

# sorting Vector{Missing} is O(1)
julia> @btime sort!(x) setup=(x=fill(missing, 100));
  163.465 ns (3 allocations: 144 bytes)  =>  4.329 ns (0 allocations: 0 bytes)

# sort(rand(1000)) is comparable
julia> @btime sort(x) setup=(x=rand(1000));
  16.383 μs (4 allocations: 18.09 KiB)  =>  16.369 μs (3 allocations: 18.06 KiB)

# sort(randn(1000)) allocates less
julia> @btime sort(x) setup=(x=randn(1000));
  23.551 μs (7 allocations: 26.00 KiB)  =>  22.088 μs (4 allocations: 14.12 KiB)

# non-IEEEFloat unions with missing are faster [fix regression]
julia> @btime sort(x) setup=(x=shuffle!(vcat(fill(missing, 10), rand(Int, 100))));
  4.265 μs (2 allocations: 2.12 KiB)  =>  2.596 μs (2 allocations: 1.94 KiB)

# IEEEFloat unions with missing are faster [fix regression]
julia> @btime sort(x) setup=(x=shuffle!(vcat(fill(missing, 10), rand(100))));
  4.322 μs (22 allocations: 4.62 KiB)  =>  2.728 μs (2 allocations: 1.94 KiB)

# non-IEEEFloat unions with missing are faster for large vectors
julia> @btime sort(x) setup=(x=shuffle!(vcat(fill(missing, 10), rand(Int, 1000))));
  60.020 μs (2 allocations: 18.12 KiB)  =>  25.735 μs (3 allocations: 19.19 KiB)

# IEEEFloat unions with missing are faster for large vectors
julia> @btime sort(x) setup=(x=shuffle!(vcat(fill(missing, 10), rand(1000))));
  56.334 μs (283 allocations: 41.72 KiB)  =>  22.828 μs (3 allocations: 19.19 KiB)

# sortperm is relatively unaffected
julia> @btime sortperm(x) setup=(x=rand(1000));
  32.822 μs (6 allocations: 23.86 KiB)  =>  32.082 μs (5 allocations: 15.92 KiB)

# sort(x; dims) is _way_ faster [fix regression]
julia> for i in 1:3 @btime sort(rand(100,100,50); dims=$i) end
  896.157 ms (10007 allocations: 18.63 GiB)  =>  13.005 ms (7 allocations: 7.63 MiB)
  953.649 ms (10022 allocations: 18.64 GiB)  =>  14.739 ms (22 allocations: 11.45 MiB)
  2.477 s (20022 allocations: 37.26 GiB)  =>  12.357 ms (21 allocations: 11.45 MiB)

# sort!(x; dims) is a bit faster
julia> for i in 1:3 @btime sort!(rand(100,100,50); dims=$i) end
  15.039 ms (15003 allocations: 8.70 MiB)  =>  13.466 ms (15003 allocations: 4.27 MiB)
  15.512 ms (15003 allocations: 8.70 MiB)  =>  14.879 ms (15003 allocations: 4.27 MiB)
  11.899 ms (20003 allocations: 5.04 MiB)  =>  10.915 ms (3 allocations: 3.82 MiB)

Fixes #47721
Unrelatedly, fixes #47474, closes #47383

@LilithHafner LilithHafner added the domain:sorting Put things in order label Oct 29, 2022
@LilithHafner LilithHafner force-pushed the sort-dispatch branch 2 times, most recently from 12d136b to 16fc0a0 Compare October 29, 2022 13:04
@petvana
Copy link
Member

petvana commented Oct 29, 2022

Nice.

There seems to be a performance regression for sortperm. This may be caused by removing fpsort!.

julia> @btime sortperm(x) setup=(x=rand(1000));
  29.040 μs (6 allocations: 23.86 KiB) =>   57.107 μs (5 allocations: 15.92 KiB)

(Btw, it seems possible to speed up sortperm by utilizing stable sort in future.)

@LilithHafner
Copy link
Member Author

Good catch. There was a typo that prevented proper dispatch. Fixing the typo resolves almost all of the regression.

julia> @btime sortperm(x) setup=(x=rand(1000));
  31.543 μs (6 allocations: 23.86 KiB)  =>  61.839 μs (5 allocations: 15.92 KiB)  =>  36.477 μs (9 allocations: 16.08 KiB)

base/sort.jl Outdated Show resolved Hide resolved
base/sort.jl Outdated
Finally, if the input has length less than 80, we dispatch to [`InsertionSort`](@ref) and
otherwise we dispatch to [`QuickSort`](@ref).
"""
const DEFAULT_STABLE = InitialOptimizations(IsUIntMappable(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems quite hard to read (and modify). Would it be possible to simplify it for the reader, smth like as follows?

const DEFAULT_STABLE = begin
          alg1 = QuickSort |> Small{80} |> ConsiderRadixSort |> ConsiderCountingSort |> ComputeExtrema |> CheckSorted |> Small{40} 
          IsUIntMappable(alg1, StableCheckSorted(QuickSort)) |> InitialOptimizations
       end

Also the outputed algorithm is kind of unusefull for the user :-D

julia> Base.Sort.defalg([1,2])
Base.Sort.MissingOptimization{Base.Sort.BoolOptimization{Small{10, Base.Sort.InsertionSortAlg, Base.Sort.IEEEFloatOptimization{Base.Sort.IsUIntMappable{Small{40, Base.Sort.InsertionSortAlg, Base.Sort.CheckSorted{Base.Sort.ComputeExtrema{Base.Sort.ConsiderCountingSort{Base.Sort.CountingSort, ConsiderRadixSort{Base.Sort.RadixSort, Small{80, Base.Sort.InsertionSortAlg, PartialQuickSort{Missing, Missing, Base.Sort.InsertionSortAlg}}}}}}}, Base.Sort.StableCheckSorted{PartialQuickSort{Missing, Missing, Base.Sort.InsertionSortAlg}}}}}}}(Base.Sort.BoolOptimization{Small{10, Base.Sort.InsertionSortAlg, Base.Sort.IEEEFloatOptimization{Base.Sort.IsUIntMappable{Small{40, Base.Sort.InsertionSortAlg, Base.Sort.CheckSorted{Base.Sort.ComputeExtrema{Base.Sort.ConsiderCountingSort{Base.Sort.CountingSort, ConsiderRadixSort{Base.Sort.RadixSort, Small{80, Base.Sort.InsertionSortAlg, PartialQuickSort{Missing, Missing, Base.Sort.InsertionSortAlg}}}}}}}, Base.Sort.StableCheckSorted{PartialQuickSort{Missing, Missing, Base.Sort.InsertionSortAlg}}}}}}(Small{10, Base.Sort.InsertionSortAlg, Base.Sort.IEEEFloatOptimization{Base.Sort.IsUIntMappable{Small{40, Base.Sort.InsertionSortAlg, Base.Sort.CheckSorted{Base.Sort.ComputeExtrema{Base.Sort.ConsiderCountingSort{Base.Sort.CountingSort, ConsiderRadixSort{Base.Sort.RadixSort, Small{80, Base.Sort.InsertionSortAlg, PartialQuickSort{Missing, Missing, Base.Sort.InsertionSortAlg}}}}}}}, Base.Sort.StableCheckSorted{PartialQuickSort{Missing, Missing, Base.Sort.InsertionSortAlg}}}}}(Base.Sort.InsertionSortAlg(), Base.Sort.IEEEFloatOptimization{Base.Sort.IsUIntMappable{Small{40, Base.Sort.InsertionSortAlg, Base.Sort.CheckSorted{Base.Sort.ComputeExtrema{Base.Sort.ConsiderCountingSort{Base.Sort.CountingSort, ConsiderRadixSort{Base.Sort.RadixSort, Small{80, Base.Sort.InsertionSortAlg, PartialQuickSort{Missing, Missing, Base.Sort.InsertionSortAlg}}}}}}}, Base.Sort.StableCheckSorted{PartialQuickSort{Missing, Missing, Base.Sort.InsertionSortAlg}}}}(Base.Sort.IsUIntMappable{Small{40, Base.Sort.InsertionSortAlg, Base.Sort.CheckSorted{Base.Sort.ComputeExtrema{Base.Sort.ConsiderCountingSort{Base.Sort.CountingSort, ConsiderRadixSort{Base.Sort.RadixSort, Small{80, Base.Sort.InsertionSortAlg, PartialQuickSort{Missing, Missing, Base.Sort.InsertionSortAlg}}}}}}}, Base.Sort.StableCheckSorted{PartialQuickSort{Missing, Missing, Base.Sort.InsertionSortAlg}}}(Small{40, Base.Sort.InsertionSortAlg, Base.Sort.CheckSorted{Base.Sort.ComputeExtrema{Base.Sort.ConsiderCountingSort{Base.Sort.CountingSort, ConsiderRadixSort{Base.Sort.RadixSort, Small{80, Base.Sort.InsertionSortAlg, PartialQuickSort{Missing, Missing, Base.Sort.InsertionSortAlg}}}}}}}(Base.Sort.InsertionSortAlg(), Base.Sort.CheckSorted{Base.Sort.ComputeExtrema{Base.Sort.ConsiderCountingSort{Base.Sort.CountingSort, ConsiderRadixSort{Base.Sort.RadixSort, Small{80, Base.Sort.InsertionSortAlg, PartialQuickSort{Missing, Missing, Base.Sort.InsertionSortAlg}}}}}}(Base.Sort.ComputeExtrema{Base.Sort.ConsiderCountingSort{Base.Sort.CountingSort, ConsiderRadixSort{Base.Sort.RadixSort, Small{80, Base.Sort.InsertionSortAlg, PartialQuickSort{Missing, Missing, Base.Sort.InsertionSortAlg}}}}}(Base.Sort.ConsiderCountingSort{Base.Sort.CountingSort, ConsiderRadixSort{Base.Sort.RadixSort, Small{80, Base.Sort.InsertionSortAlg, PartialQuickSort{Missing, Missing, Base.Sort.InsertionSortAlg}}}}(Base.Sort.CountingSort(), ConsiderRadixSort{Base.Sort.RadixSort, Small{80, Base.Sort.InsertionSortAlg, PartialQuickSort{Missing, Missing, Base.Sort.InsertionSortAlg}}}(Base.Sort.RadixSort(), Small{80, Base.Sort.InsertionSortAlg, PartialQuickSort{Missing, Missing, Base.Sort.InsertionSortAlg}}(Base.Sort.InsertionSortAlg(), PartialQuickSort{Missing, Missing, Base.Sort.InsertionSortAlg}(missing, missing, Base.Sort.InsertionSortAlg()))))))), Base.Sort.StableCheckSorted{PartialQuickSort{Missing, Missing, Base.Sort.InsertionSortAlg}}(PartialQuickSort{Missing, Missing, Base.Sort.InsertionSortAlg}(missing, missing, Base.Sort.InsertionSortAlg())))))))

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah... that output is especially terrible.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer not to reverse the order with |> because the standard order reflects the order of operations. MissingOptimization(InsertionSort()) first tries to perform a missing optimization and then performs insertion sort.

I've reformatted the const declarations and added pretty printing:

const DEFAULT_STABLE = InitialOptimizations(
    IsUIntMappable(
        Small{40}(
            CheckSorted(
                ComputeExtrema(
                    ConsiderCountingSort(
                        ConsiderRadixSort(
                            Small{80}(
                                QuickSort)))))),
        StableCheckSorted(
            QuickSort)))

and

julia> Base.DEFAULT_STABLE
Base.Sort.MissingOptimization(
    Base.Sort.BoolOptimization(
        Base.Sort.Small{10}(
            InsertionSort(),
            Base.Sort.IEEEFloatOptimization(
                Base.Sort.IsUIntMappable(
                    Base.Sort.Small{40}(
                        InsertionSort(),
                        Base.Sort.CheckSorted(
                            Base.Sort.ComputeExtrema(
                                Base.Sort.ConsiderCountingSort(
                                    Base.Sort.CountingSort(),
                                    Base.Sort.ConsiderRadixSort(
                                        Base.Sort.RadixSort(),
                                        Base.Sort.Small{80}(
                                            InsertionSort(),
                                            PartialQuickSort(missing, missing,
                                                InsertionSort()))))))),
                    Base.Sort.StableCheckSorted(
                        PartialQuickSort(missing, missing,
                            InsertionSort())))))))

It's still fairly large, but I don't think there's any way of getting around that without either simplifying dispatch (possible a good idea for a future PR) or moving many optimization passes outside the scope of DEFAULT_STABLE (and therefore not easily configured by a user)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've reformatted the const declarations and added pretty printing:

Thanks, it's definitely better. Btw, this seems like a nice case for reversed <| operator.

if _issorted(v, lo, hi, o)
return v
elseif _issorted(v, lo, hi, Lt((x, y) -> !lt(o, x, y)))
# Reverse only if necessary. Using issorted(..., Reverse(o)) would violate stability.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems possible to use issorted(..., Reverse(o)) and then implement _stablereverse!(v, lo, hi, o) that would handle the stability such that a segment of equal elements is reversed back at the end. This all is O(n) but outside of this PR's scope.

@StefanKarpinski StefanKarpinski self-assigned this Nov 2, 2022
@KristofferC
Copy link
Sponsor Member

Probably good with a PkgEval here before merging.

Lilith Hafner added 15 commits November 8, 2022 15:04
FIXES UNEXPECTED ALLOCATIONS
removes code that previously harbored bugs that slipped through the test suite
Fixes a few remaining unexpected allocations
U can be statically computed from the type of v and order so there is no need.
Further, U is infered as ::DataType rather than Type{U} which causes type instabilities.
it is invalid to cache lenm1 because lo and hi may be redefined
and we have no cache invalidation system
fixes #47474
in this PR rather than separate to avoid dealing with the merge
@LilithHafner
Copy link
Member Author

NFC rebase to clean up the history a bit and get up to date with master for nanosoldier.

The final commit worries me a bit; I have no idea why we need to define all four defalg methods to avoid hanging on doctests.

@nanosoldier runtests(ALL, vs = ":master")`

@nanosoldier
Copy link
Collaborator

Your package evaluation job has completed - possible new issues were detected. A full report can be found here.

@LilithHafner
Copy link
Member Author

Breaks SortingAlgorithms which has 2033 direct + indirect deps.

@LilithHafner
Copy link
Member Author

Rerunning failed PkgEval now that SortingAlgorithms 3.2 has been released to work around compat bounds restricting to below the non-breaking 1.0 release.

@nanosoldier runtests(["ARules", "JuliaDBMeta", "GEEBRA", "ParticleMDI", "MultiModalMuSig", "SortingAlgorithms", "SoilProfiles", "DeepDish", "LazyAlgebra", "GenomicVectors", "JLBoost", "TopologyPreprocessing", "MaxEntropyGraphs", "ChemometricsTools", "StaticTools", "RigorousCoupledWaveAnalysis", "DeformableBodies", "LockandKeyLookups", "RvSpectMLPlots", "HydrophoneCalibrations", "Minio", "CSVReader", "ModelConstructors", "SantasLittleHelpers", "EvaluationCF", "MLJJLBoost", "StanMamba", "ArgoData", "NeuralQuantumState", "Mex", "JLBoostMLJ", "DTALib", "ANOVA", "NumericalAlgorithms", "InvariantCausal", "RenderJay", "FilteredGroupbyMacro", "MaximumLikelihoodProblems", "REDCap", "OpSel", "UAParser", "PkgDependency", "RigidBodyTools", "TheCannon", "ProgenyTestingTools", "ChemometricsData", "AppliGeneralLedger", "PosDefManifoldML", "VariantCallFormat", "Schrute", "Whitebox", "CORBITS", "KeyedFrames", "ReferenceTests", "Recommenders", "Hadleyverse", "QuerySQLite", "MotifRecognition", "OptimalTransmissionRouting", "OMOPCommonDataModel", "LITS", "EchogramImages", "Phylo", "CrossfilterCharts", "DPClustering", "ClimateTasks", "StanDataFrames", "SparseRegression", "JSONLines", "LifeTable", "Hydrographs", "Firefly", "Immerse", "GlobalSearchRegression", "Infernal"])

@nanosoldier
Copy link
Collaborator

Your package evaluation job has completed - possible new issues were detected. A full report can be found here.

@nanosoldier
Copy link
Collaborator

Your package evaluation job has completed - possible new issues were detected. A full report can be found here.

@LilithHafner
Copy link
Member Author

LilithHafner commented Dec 1, 2022

A total of 4 PkgEval failures remain, three of which have identified non-blocking causes and two of which seem unrelated but have no known cause.

Package has test failures (3 packages):
Minio v0.2.0: fail vs. ok - Networking

Recommenders v0.1.2: fail vs. ok - Error while sorting a SparseArray that previously produced a dense result. There is an open PR to fix this, but a maintainer has indicated that they prefer an error in this case, so I do not view this as a regression.

SortingAlgorithms v1.1.0: fail vs. ok - Due to testing that HeapSort, and unstable sorting algorithm, preserves the order of input NaNs. We can loosen the test or adjust the hook into Julia's internals to maintain this property, but either way it is not a behavior that anyone downstream depends on (at least not in PkgEval).

There were unidentified errors (1 packages):
JSONLines v2.0.1: fail vs. ok - Seems entirely unrelated; rerunning

I'm going to investigate the final failure further, but unless I find a regression related to this PR or anyone objects, I plan to merge this soon.

This is the diff since @KristoferC's "Probably good with a PkgEval here before merging."

It contains added tests and bug fixes from PkgEval, simplifications, renaming buffer => scratch, and better scratch space handling to architecture to reduce allocations and support future improvements.

@LilithHafner
Copy link
Member Author

Rerunning the package whose test failure confuses me: @nanosoldier runtests(["JSONLines"])

@nanosoldier
Copy link
Collaborator

Your package evaluation job has completed - no new issues were detected. A full report can be found here.

@LilithHafner
Copy link
Member Author

LilithHafner commented Dec 2, 2022

I also couldn't reproduce that package's test failures locally, so this LGTM

@LilithHafner LilithHafner merged commit cee0a04 into master Dec 3, 2022
@LilithHafner LilithHafner deleted the sort-dispatch branch December 3, 2022 00:19
@LilithHafner LilithHafner mentioned this pull request Dec 4, 2022
@LilithHafner LilithHafner restored the sort-dispatch branch December 5, 2022 03:20
@LilithHafner LilithHafner deleted the sort-dispatch branch December 5, 2022 03:21
@LilithHafner
Copy link
Member Author

I expect this to fail; though it worked on the prior commit: @nanosoldier runbenchmarks("sort")

@nanosoldier
Copy link
Collaborator

Your job failed.

KristofferC pushed a commit that referenced this pull request Dec 8, 2022
* create an internal `_sort!` function and use it (rename the existing `_sort!` to `__sort!`)

* test for several of bugs that slipped through test suite

* Give each sorting pass and DEFAULT_STABLE a docstring

* add pretty printing for the new algorithms that are much more flexible than the old ones

* fix unexpected allocations in Radix Sort

fixes #47474
in this PR rather than separate to avoid dealing with the merge

* support and test backwards compatibility with packages that depend in sorting internals

* support 3-, 5-, and 6-argument sort! for backwards compatibility

* overhall scratch space handling

make _sort! return scratch space rather than sorted vector
so that things like IEEEFloatOptimization can re-use the
scratch space allocated on their first recursive call

* test handling -0.0 in IEEEFloatOptimization

* fix and test bug where countsort's correct overflow behavior triggers error due to unexpected promotion to UInt

(cherry picked from commit cee0a04)
@LilithHafner
Copy link
Member Author

This PR broke runbenchmarks("sort"); #47822 fixes it.

KristofferC pushed a commit that referenced this pull request Dec 13, 2022
* add test demonstrating overflow in countsort

* fix overflow in countsort

* remove unnecessary type annotations (fixes tests)

This fixes the test failure because it allows for automatic conversion.
The manual for implementing the AbstractArray interface also does not recomend
a type signature for the value arg in setindex!.

Co-authored-by: Lilith Hafner <Lilith.Hafner@gmail.com>
@vtjnash
Copy link
Sponsor Member

vtjnash commented Dec 13, 2022

Did this end up getting any nanosoldier testing? quicksort performance seemed to do very poorly on the latest nightly, which seems likely this commit to be the culprit

@LilithHafner
Copy link
Member Author

No it didn't and yikes those regressions are bad.

KristofferC pushed a commit that referenced this pull request Dec 14, 2022
* add test demonstrating overflow in countsort

* fix overflow in countsort

* remove unnecessary type annotations (fixes tests)

This fixes the test failure because it allows for automatic conversion.
The manual for implementing the AbstractArray interface also does not recomend
a type signature for the value arg in setindex!.

Co-authored-by: Lilith Hafner <Lilith.Hafner@gmail.com>
(cherry picked from commit 965bc7d)
@KristofferC KristofferC mentioned this pull request Dec 14, 2022
26 tasks
@LilithHafner
Copy link
Member Author

Just looked into this and the reason we have regressions is that this PR makes QuickSort specify using exactly and only the quicksort sorting method. Notably, this excludes floating pojnt optimizations and presorted checks which results in huge regressions for presorted floating point inputs (including ascending, descending, and all ones, which is also sorted) and substantial regressions for random floating point inputs.

We didn't catch these regression earlier because they only effect usage that explicitly specifies alg=QuickSort, alg=MergeSort, etc. and the more worrying regression only comes up for floating points. BaseBenchmarks benchmarks (only) the use case of sorting floating point vectors with explicitly specified algorithms. After JuliaCI/BaseBenchmarks.jl#305, it will continue to benchmark that case and also other cases.

@KristofferC KristofferC removed the backport 1.9 Change should be backported to release-1.9 label Dec 27, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
domain:sorting Put things in order
Projects
None yet
7 participants