From 6230587cf891bf56d2e3cf0ec3a9b54537dd357a Mon Sep 17 00:00:00 2001 From: Lilith Hafner Date: Sat, 7 May 2022 08:36:42 -0500 Subject: [PATCH 01/30] Stabilize and optimize QuickSort --- base/sort.jl | 296 ++++++++++++++++++++++++------------------------ test/sorting.jl | 37 ++++-- 2 files changed, 177 insertions(+), 156 deletions(-) diff --git a/base/sort.jl b/base/sort.jl index 23579abd77547..729b67f610387 100644 --- a/base/sort.jl +++ b/base/sort.jl @@ -5,15 +5,13 @@ module Sort import ..@__MODULE__, ..parentmodule const Base = parentmodule(@__MODULE__) using .Base.Order -using .Base: copymutable, LinearIndices, length, (:), iterate, - eachindex, axes, first, last, similar, zip, OrdinalRange, - AbstractVector, @inbounds, AbstractRange, @eval, @inline, Vector, @noinline, - AbstractMatrix, AbstractUnitRange, isless, identity, eltype, >, <, <=, >=, |, +, -, *, !, - extrema, sub_with_overflow, add_with_overflow, oneunit, div, getindex, setindex!, - length, resize!, fill, Missing, require_one_based_indexing, keytype, UnitRange, - min, max, reinterpret, signed, unsigned, Signed, Unsigned, typemin, xor, Type, BitSigned - -using .Base: >>>, !== +using .Base: length, first, last, axes, eltype, similar, iterate, keytype, copymutable, + fill, eachindex, zip, copyto!, resize!, LinearIndices, require_one_based_indexing, + AbstractVector, Vector, AbstractRange, OrdinalRange, UnitRange, + identity, isless, min, max, extrema, sub_with_overflow, add_with_overflow, oneunit, + reinterpret, signed, unsigned, Signed, Unsigned, typemin, Type, BitSigned, + Missing, missing, ismissing, @eval, @inbounds, @inline, @noinline, + (:), >, <, <=, >=, ==, ===, |, +, -, *, !, <<, >>, &, >>>, !==, div, xor import .Base: sort, @@ -94,8 +92,7 @@ issorted(itr; issorted(itr, ord(lt,by,rev,order)) function partialsort!(v::AbstractVector, k::Union{Integer,OrdinalRange}, o::Ordering) - inds = axes(v, 1) - sort!(v, first(inds), last(inds), PartialQuickSort(k), o) + sort!(v, PartialQuickSort(k), o) maybeview(v, k) end @@ -423,51 +420,37 @@ insorted(x, r::AbstractRange) = in(x, r) abstract type Algorithm end struct InsertionSortAlg <: Algorithm end -struct QuickSortAlg <: Algorithm end struct MergeSortAlg <: Algorithm end +struct AdaptiveSortAlg <: Algorithm end """ - AdaptiveSort(fallback) + PartialQuickSort(lo::Union{Integer, Missing}, hi::Union{Integer, Missing}) -Indicate that a sorting function should use the fastest available algorithm. +Indicate that a sorting function should use the partial quick sort algorithm. -Adaptive sort will use the algorithm specified by `fallback` for types and orders that are -not [`UIntMappable`](@ref). Otherwise, it will typically use: - * Insertion sort for short vectors - * Radix sort for long vectors - * Counting sort for vectors of integers spanning a short range - -Adaptive sort is guaranteed to be stable if the fallback algorithm is stable. -""" -struct AdaptiveSort{Fallback <: Algorithm} <: Algorithm - fallback::Fallback -end -""" - PartialQuickSort{T <: Union{Integer,OrdinalRange}} - -Indicate that a sorting function should use the partial quick sort -algorithm. Partial quick sort returns the smallest `k` elements sorted from smallest -to largest, finding them and sorting them using [`QuickSort`](@ref). +Partial quick sort finds and sorts the elements that would end up in positions +`lo:hi` using [`QuickSort`](@ref). Characteristics: - * *not stable*: does not preserve the ordering of elements which - compare equal (e.g. "a" and "A" in a sort of letters which - ignores case). - * *in-place* in memory. + * *stable*: preserves the ordering of elements which compare equal + (e.g. "a" and "A" in a sort of letters which ignores case). + * *not in-place* in memory. * *divide-and-conquer*: sort strategy similar to [`MergeSort`](@ref). """ -struct PartialQuickSort{T <: Union{Integer,OrdinalRange}} <: Algorithm - k::T +struct PartialQuickSort{L<:Union{Integer,Missing}, H<:Union{Integer,Missing}} <: Algorithm + lo::L + hi::H end - +PartialQuickSort(k::Integer) = PartialQuickSort(missing, k) +PartialQuickSort(k::OrdinalRange) = PartialQuickSort(first(k), last(k)) """ InsertionSort -Indicate that a sorting function should use the insertion sort -algorithm. Insertion sort traverses the collection one element -at a time, inserting each element into its correct, sorted position in -the output vector. +Indicate that a sorting function should use the insertion sort algorithm. + +Insertion sort traverses the collection one element at a time, inserting +each element into its correct, sorted position in the output vector. Characteristics: * *stable*: preserves the ordering of elements which @@ -478,29 +461,34 @@ Characteristics: it is well-suited to small collections but should not be used for large ones. """ const InsertionSort = InsertionSortAlg() + """ QuickSort -Indicate that a sorting function should use the quick sort -algorithm, which is *not* stable. +Indicate that a sorting function should use the quick sort algorithm. + +Quick sort picks a pivot element, partitions the array based on the pivot, +and then sorts the elements before and after the pivot recursively. Characteristics: - * *not stable*: does not preserve the ordering of elements which - compare equal (e.g. "a" and "A" in a sort of letters which - ignores case). - * *in-place* in memory. + * *stable*: preserves the ordering of elements which compare equal + (e.g. "a" and "A" in a sort of letters which ignores case). + * *not in-place* in memory. * *divide-and-conquer*: sort strategy similar to [`MergeSort`](@ref). - * *good performance* for large collections. + * *good performance* for most large collections. + * *quadratic worst case runtime* in pathological cases + (vanishingly rare for non-malicious input) """ -const QuickSort = QuickSortAlg() +const QuickSort = PartialQuickSort(missing, missing) + """ MergeSort -Indicate that a sorting function should use the merge sort -algorithm. Merge sort divides the collection into -subcollections and repeatedly merges them, sorting each -subcollection at each step, until the entire -collection has been recombined in sorted form. +Indicate that a sorting function should use the merge sort algorithm. + +Merge sort divides the collection into subcollections and +repeatedly merges them, sorting each subcollection at each step, +until the entire collection has been recombined in sorted form. Characteristics: * *stable*: preserves the ordering of elements which compare @@ -509,10 +497,23 @@ Characteristics: * *not in-place* in memory. * *divide-and-conquer* sort strategy. """ -const MergeSort = MergeSortAlg() +const MergeSort = MergeSortAlg() + +""" + AdaptiveSort + +Indicate that a sorting function should use the fastest available stable algorithm. + +Currently, AdaptiveSort uses + * [`InsertionSort`](@ref) for short vectors + * [`QuickSort`](@ref) for vectors that are not [`UIntMappable`](@ref) + * Radix sort for long vectors + * Counting sort for vectors of integers spanning a short range +""" +const AdaptiveSort = AdaptiveSortAlg() -const DEFAULT_UNSTABLE = AdaptiveSort(QuickSort) -const DEFAULT_STABLE = AdaptiveSort(MergeSort) +const DEFAULT_UNSTABLE = AdaptiveSort +const DEFAULT_STABLE = AdaptiveSort const SMALL_ALGORITHM = InsertionSort const SMALL_THRESHOLD = 20 @@ -529,75 +530,107 @@ function sort!(v::AbstractVector, lo::Integer, hi::Integer, ::InsertionSortAlg, return v end -# selectpivot! +# TODO Stable doesnt make sense because rev may be true!!!! +# return the index of the stable median of v[lo], v[mi], and v[hi] +# that is, compare the elements and break ties using the index # -# Given 3 locations in an array (lo, mi, and hi), sort v[lo], v[mi], v[hi]) and -# choose the middle value as a pivot +# for example, the stable median of [4, 4, 5] is the second +# element and the stable median of [4, 4, 3] is the first element # -# Upon return, the pivot is in v[lo], and v[hi] is guaranteed to be -# greater than the pivot - -@inline function selectpivot!(v::AbstractVector, lo::Integer, hi::Integer, o::Ordering) - @inbounds begin - mi = midpoint(lo, hi) - - # sort v[mi] <= v[lo] <= v[hi] such that the pivot is immediately in place - if lt(o, v[lo], v[mi]) - v[mi], v[lo] = v[lo], v[mi] - end +# TODO replace with rand(lo:hi) if Random is ever a part of Base +function select_pivot(v::AbstractVector, lo::Integer, hi::Integer, o::Ordering) + mi = midpoint(lo, hi) + @inbounds a, b, c = v[lo], v[mi], v[hi] + + if lt(o, b, a) + b, a = a, b + lo, mi = mi, lo + end - if lt(o, v[hi], v[lo]) - if lt(o, v[hi], v[mi]) - v[hi], v[lo], v[mi] = v[lo], v[mi], v[hi] - else - v[hi], v[lo] = v[lo], v[hi] - end - end + # a is stably less than b - # return the pivot - return v[lo] + if lt(o, c, a) + lo + elseif lt(o, c, b) + hi + else + mi end end -# partition! +# select a pivot, partition v[lo:hi] according +# to the pivot, and store the result in t[lo:hi]. # -# select a pivot, and partition v according to the pivot - -function partition!(v::AbstractVector, lo::Integer, hi::Integer, o::Ordering) - pivot = selectpivot!(v, lo, hi, o) - # pivot == v[lo], v[hi] > pivot - i, j = lo, hi - @inbounds while true - i += 1; j -= 1 - while lt(o, v[i], pivot); i += 1; end; - while lt(o, pivot, v[j]); j -= 1; end; - i >= j && break - v[i], v[j] = v[j], v[i] +# returns (pivot, pivot_index) where pivot_index is the location the pivot +# should end up, but does not set t[pivot_index] = pivot +function partition!(t::AbstractVector, lo::Integer, hi::Integer, o::Ordering, v::AbstractVector, rev::Bool) + pivot_index = select_pivot(v, lo, hi, o) + trues = 0 + @inbounds begin + pivot = v[pivot_index] + while lo < pivot_index + x = v[lo] + fx = rev ? !lt(o, x, pivot) : lt(o, pivot, x) + t[(fx ? hi : lo) - trues] = x + trues += fx + lo += 1 + end + while lo < hi + x = v[lo+1] + fx = rev ? lt(o, pivot, x) : !lt(o, x, pivot) + t[(fx ? hi : lo) - trues] = x + trues += fx + lo += 1 + end end - v[j], v[lo] = pivot, v[j] - # v[j] == pivot - # v[k] >= pivot for k > j - # v[i] <= pivot for i < j - return j + # pivot_index = lo-trues + # t[pivot_index] is whatever it was before + # t[pivot_index] >* pivot, reverse stable + + pivot, lo-trues end -function sort!(v::AbstractVector, lo::Integer, hi::Integer, a::QuickSortAlg, o::Ordering) - @inbounds while lo < hi - hi-lo <= SMALL_THRESHOLD && return sort!(v, lo, hi, SMALL_ALGORITHM, o) - j = partition!(v, lo, hi, o) - if j-lo < hi-j - # recurse on the smaller chunk - # this is necessary to preserve O(log(n)) - # stack space in the worst case (rather than O(n)) - lo < (j-1) && sort!(v, lo, j-1, a, o) +# Needed for bootstrapping because `view` is not available. +function reverse_view!(v, lo, hi) + for i in 0:div(hi-lo-1, 2) + @inbounds v[lo+i], v[hi-i] = v[hi-i], v[lo+i] + end + v +end + +function sort!(v::AbstractVector, lo::Integer, hi::Integer, a::PartialQuickSort, o::Ordering, t::AbstractVector=similar(v), swap=false, rev=false) + while lo < hi && hi - lo > SMALL_THRESHOLD + pivot, j = swap ? partition!(v, lo, hi, o, t, rev) : partition!(t, lo, hi, o, v, rev) + @inbounds v[j] = pivot + swap = !swap + + # For QuickSort, a.lo === a.hi === missing, so the first two branches get skipped + if !ismissing(a.lo) && j <= a.lo # Skip sorting the lower part + swap && copyto!(v, lo, t, lo, j-lo) + rev && reverse_view!(v, lo, j-1) lo = j+1 - else - j+1 < hi && sort!(v, j+1, hi, a, o) + rev = !rev + elseif !ismissing(a.hi) && a.hi <= j # Skip sorting the upper part + swap && copyto!(v, j+1, t, j+1, hi-j) + rev || reverse_view!(v, j+1, hi) + hi = j-1 + elseif j-lo < hi-j + # Sort the lower part recursively because it is smaller. Recursing on the + # smaller part guarantees O(log(n)) stack space even on pathological inputs. + sort!(v, lo, j-1, a, o, t, swap, rev) + lo = j+1 + rev = !rev + else # Sort the higher part recursively + sort!(v, j+1, hi, a, o, t, swap, !rev) hi = j-1 end end - return v + hi < lo && return v + swap && copyto!(v, lo, t, lo, hi-lo+1) + rev && reverse_view!(v, lo, hi) + sort!(v, lo, hi, SMALL_ALGORITHM, o) end function sort!(v::AbstractVector, lo::Integer, hi::Integer, a::MergeSortAlg, o::Ordering, t=similar(v,0)) @@ -638,32 +671,6 @@ function sort!(v::AbstractVector, lo::Integer, hi::Integer, a::MergeSortAlg, o:: return v end -function sort!(v::AbstractVector, lo::Integer, hi::Integer, a::PartialQuickSort, - o::Ordering) - @inbounds while lo < hi - hi-lo <= SMALL_THRESHOLD && return sort!(v, lo, hi, SMALL_ALGORITHM, o) - j = partition!(v, lo, hi, o) - - if j <= first(a.k) - lo = j+1 - elseif j >= last(a.k) - hi = j-1 - else - # recurse on the smaller chunk - # this is necessary to preserve O(log(n)) - # stack space in the worst case (rather than O(n)) - if j-lo < hi-j - lo < (j-1) && sort!(v, lo, j-1, a, o) - lo = j+1 - else - hi > (j+1) && sort!(v, j+1, hi, a, o) - hi = j-1 - end - end - end - return v -end - # This is a stable least significant bit first radix sort. # # That is, it first sorts the entire vector by the last chunk_size bits, then by the second @@ -732,7 +739,7 @@ end # For AbstractVector{Bool}, counting sort is always best. # This is an implementation of counting sort specialized for Bools. -function sort!(v::AbstractVector{<:Bool}, lo::Integer, hi::Integer, a::AdaptiveSort, o::Ordering) +function sort!(v::AbstractVector{<:Bool}, lo::Integer, hi::Integer, ::AdaptiveSortAlg, o::Ordering) first = lt(o, false, true) ? false : lt(o, true, false) ? true : return v count = 0 @inbounds for i in lo:hi @@ -756,11 +763,11 @@ function _extrema(v::AbstractArray, lo::Integer, hi::Integer, o::Ordering) end mn, mx end -function sort!(v::AbstractVector, lo::Integer, hi::Integer, a::AdaptiveSort, o::Ordering) +function sort!(v::AbstractVector, lo::Integer, hi::Integer, ::AdaptiveSortAlg, o::Ordering) # if the sorting task is not UIntMappable, then we can't radix sort or sort_int_range! # so we skip straight to the fallback algorithm which is comparison based. U = UIntMappable(eltype(v), o) - U === nothing && return sort!(v, lo, hi, a.fallback, o) + U === nothing && return sort!(v, lo, hi, QuickSort, o) # to avoid introducing excessive detection costs for the trivial sorting problem # and to avoid overflow, we check for small inputs before any other runtime checks @@ -795,7 +802,7 @@ function sort!(v::AbstractVector, lo::Integer, hi::Integer, a::AdaptiveSort, o:: return sort_int_range!(v, Int(v_range+1), v_min, o === Forward ? identity : reverse, lo, hi) end end - return sort!(v, lo, hi, a.fallback, o) + return sort!(v, lo, hi, QuickSort, o) end v_min, v_max = _extrema(v, lo, hi, o) @@ -830,8 +837,7 @@ function sort!(v::AbstractVector, lo::Integer, hi::Integer, a::AdaptiveSort, o:: # lenm1 < 3bits if lenm1 < 3bits # at lenm1 = 64*3-1, QuickSort is about 20% faster than InsertionSort. - alg = a.fallback === QuickSort && lenm1 > 120 ? QuickSort : SMALL_ALGORITHM - return sort!(v, lo, hi, alg, o) + return sort!(v, lo, hi, lenm1 > 120 ? QuickSort : SMALL_ALGORITHM, o) end # At this point, we are committed to radix sort. @@ -1529,10 +1535,6 @@ function fpsort!(v::AbstractVector, a::Algorithm, o::Ordering) return v end - -fpsort!(v::AbstractVector, a::Sort.PartialQuickSort, o::Ordering) = - sort!(v, first(axes(v,1)), last(axes(v,1)), a, o) - sort!(v::FPSortable, a::Algorithm, o::DirectOrdering) = fpsort!(v, a, o) sort!(v::AbstractVector{<:Union{Signed, Unsigned}}, a::Algorithm, o::Perm{<:DirectOrdering,<:FPSortable}) = diff --git a/test/sorting.jl b/test/sorting.jl index 2cb4eec93b380..ba0c796ad27f3 100644 --- a/test/sorting.jl +++ b/test/sorting.jl @@ -30,6 +30,17 @@ end @test -1 <= Base.Sort.midpoint(typemin(Int), typemax(Int)) <= 0 end +@testset "select_pivot" begin + @test Base.Sort.select_pivot([1, 1, 1], 1, 3, Forward) == 2 + @test Base.Sort.select_pivot([1, 3, 1], 1, 3, Reverse) == 1 + @test Base.Sort.select_pivot([2, 2, 3], 1, 3, Forward) == 2 + @test Base.Sort.select_pivot([2, 3, 1, 3, 1], 1, 5, Forward) == 5 + @test Base.Sort.select_pivot([2, 3, 1, 4, 2], 3, 5, Reverse) == 5 + @test Base.Sort.select_pivot([2, 2, 1], 1, 3, Forward) == 1 + @test Base.Sort.select_pivot([2, 2, 1], 1, 3, By(x -> 0)) == 2 + @test Base.Sort.select_pivot([3, 2, 1], 1, 3, Reverse) == 2 +end + @testset "sort" begin @test sort([2,3,1]) == [1,2,3] == sort([2,3,1]; order=Forward) @test sort([2,3,1], rev=true) == [3,2,1] == sort([2,3,1], order=Reverse) @@ -61,6 +72,14 @@ end @test issorted(sort(rand(UInt64(1):UInt64(2), 7); rev=true); rev=true) # issue #43034 end +@testset "stability" begin + for Alg in [InsertionSort, MergeSort, QuickSort, Base.Sort.AdaptiveSort, Base.DEFAULT_STABLE, + PartialQuickSort(missing, 1729), PartialQuickSort(1729, missing)] + @test issorted(sort(1:2000, alg=Alg, by=x->0)) + @test issorted(sort(1:2000, alg=Alg, by=x->x÷100)) + end +end + @testset "partialsort" begin @test partialsort([3,6,30,1,9],3) == 6 @test partialsort([3,6,30,1,9],3:4) == [6,9] @@ -265,7 +284,7 @@ Base.step(r::ConstantRange) = 0 @test searchsortedlast(r, UInt(1), Forward) == 5 a = rand(1:10000, 1000) - for alg in [InsertionSort, MergeSort, Base.DEFAULT_STABLE] + for alg in [InsertionSort, MergeSort, QuickSort, Base.DEFAULT_STABLE] b = sort(a, alg=alg) @test issorted(b) @@ -402,8 +421,8 @@ end @testset "PartialQuickSort" begin a = rand(1:10000, 1000) # test PartialQuickSort only does a partial sort - let alg = PartialQuickSort(1:div(length(a), 10)) - k = alg.k + let k = 1:div(length(a), 10) + alg = PartialQuickSort(k) b = sort(a, alg=alg) c = sort(a, alg=alg, by=x->1/x) d = sort(a, alg=alg, rev=true) @@ -414,8 +433,8 @@ end @test !issorted(c, by=x->1/x) @test !issorted(d, rev=true) end - let alg = PartialQuickSort(div(length(a), 10)) - k = alg.k + let k = div(length(a), 10) + alg = PartialQuickSort(k) b = sort(a, alg=alg) c = sort(a, alg=alg, by=x->1/x) d = sort(a, alg=alg, rev=true) @@ -432,6 +451,7 @@ end @test partialsortperm([3,6,30,1,9], 2, rev=true) == 5 @test partialsortperm([3,6,30,1,9], 2, by=x->1/x) == 5 end + ## more advanced sorting tests ## randnans(n) = reinterpret(Float64,[rand(UInt64)|0x7ff8000000000000 for i=1:n]) @@ -467,7 +487,7 @@ end @test c == v # stable algorithms - for alg in [MergeSort, Base.DEFAULT_STABLE] + for alg in [MergeSort, QuickSort, Base.DEFAULT_STABLE] p = sortperm(v, alg=alg, rev=rev) p2 = sortperm(float(v), alg=alg, rev=rev) @test p == p2 @@ -511,8 +531,7 @@ end end v = randn_with_nans(n,0.1) - # TODO: alg = PartialQuickSort(n) fails here - for alg in [InsertionSort, QuickSort, MergeSort, Base.DEFAULT_UNSTABLE, Base.DEFAULT_STABLE], + for alg in [InsertionSort, MergeSort, QuickSort, PartialQuickSort(n), Base.DEFAULT_UNSTABLE, Base.DEFAULT_STABLE], rev in [false,true] alg === InsertionSort && n >= 3000 && continue # test float sorting with NaNs @@ -574,7 +593,7 @@ end @test all(issorted, [sp[inds.==x] for x in 1:200]) end - for alg in [InsertionSort, MergeSort, Base.DEFAULT_STABLE] + for alg in [InsertionSort, MergeSort, QuickSort, Base.DEFAULT_STABLE] sp = sortperm(inds, alg=alg) @test all(issorted, [sp[inds.==x] for x in 1:200]) end From 3db4a31dc33b417f6445d0f3c6973045a6602b85 Mon Sep 17 00:00:00 2001 From: Lilith Hafner Date: Sun, 8 May 2022 12:15:19 -0500 Subject: [PATCH 02/30] test invalid lt to close #11429 --- test/sorting.jl | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/test/sorting.jl b/test/sorting.jl index ba0c796ad27f3..0a61292b5a503 100644 --- a/test/sorting.jl +++ b/test/sorting.jl @@ -833,4 +833,41 @@ end end end +@testset "invalid lt (#11429)" begin + # lt must be a total linear order (e.g. < not <=) so this usage is + # not allowed. Consequently, none of the behavior tested in this + # testset is gaurunteed to work in future minor versions of Julia. + + n = 1000 + v = rand(1:5, n); + s = sort(v); + + # Nevertheless, it still works... + for alg in [InsertionSort, MergeSort, QuickSort, + Base.Sort.AdaptiveSort, Base.DEFAULT_STABLE, Base.DEFAULT_UNSTABLE] + @test sort(v, alg=alg, lt = <=) == s + end + @test partialsort(v, 172, lt = <=) == s[172] + @test partialsort(v, 315:415, lt = <=) == s[315:415] + + # ...and it is consistantly reverse stable. All these algorithms swap v[i] and v[j] + # where i < j if and only if lt(o, v[j], v[i]). This invariant holds even for + # this invalid lt order. + perm = reverse(sortperm(v, rev=true)) + for alg in [InsertionSort, MergeSort, QuickSort, + Base.Sort.AdaptiveSort, Base.DEFAULT_STABLE, Base.DEFAULT_UNSTABLE] + @test sort(1:n, alg=alg, lt = (i,j) -> v[i]<=v[j]) == perm + end + @test partialsort(1:n, 172, lt = (i,j) -> v[i]<=v[j]) == perm[172] + @test partialsort(1:n, 315:415, lt = (i,j) -> v[i]<=v[j]) == perm[315:415] + + # lt can be very poorly behaved and sort will still permute its input in some way. + for alg in [InsertionSort, MergeSort, QuickSort, + Base.Sort.AdaptiveSort, Base.DEFAULT_STABLE, Base.DEFAULT_UNSTABLE] + @test sort!(sort(v, alg=alg, lt = (x,y) -> rand([false, true]))) == s + end + @test partialsort(v, 172, lt = (x,y) -> rand([false, true])) ∈ 1:5 + @test all(partialsort(v, 315:415, lt = (x,y) -> rand([false, true])) .∈ (1:5,)) +end + end From 3318c34ead4d97ab3e0a1de11b8e57318302f8e2 Mon Sep 17 00:00:00 2001 From: Lilith Hafner Date: Sun, 8 May 2022 11:05:48 -0500 Subject: [PATCH 03/30] Remove redundant reverse_view! thanks @petvana --- base/sort.jl | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/base/sort.jl b/base/sort.jl index 729b67f610387..a39e673daffad 100644 --- a/base/sort.jl +++ b/base/sort.jl @@ -6,8 +6,8 @@ import ..@__MODULE__, ..parentmodule const Base = parentmodule(@__MODULE__) using .Base.Order using .Base: length, first, last, axes, eltype, similar, iterate, keytype, copymutable, - fill, eachindex, zip, copyto!, resize!, LinearIndices, require_one_based_indexing, - AbstractVector, Vector, AbstractRange, OrdinalRange, UnitRange, + fill, eachindex, zip, copyto!, reverse!, resize!, require_one_based_indexing, + AbstractVector, Vector, AbstractRange, OrdinalRange, UnitRange, LinearIndices, identity, isless, min, max, extrema, sub_with_overflow, add_with_overflow, oneunit, reinterpret, signed, unsigned, Signed, Unsigned, typemin, Type, BitSigned, Missing, missing, ismissing, @eval, @inbounds, @inline, @noinline, @@ -592,14 +592,6 @@ function partition!(t::AbstractVector, lo::Integer, hi::Integer, o::Ordering, v: pivot, lo-trues end -# Needed for bootstrapping because `view` is not available. -function reverse_view!(v, lo, hi) - for i in 0:div(hi-lo-1, 2) - @inbounds v[lo+i], v[hi-i] = v[hi-i], v[lo+i] - end - v -end - function sort!(v::AbstractVector, lo::Integer, hi::Integer, a::PartialQuickSort, o::Ordering, t::AbstractVector=similar(v), swap=false, rev=false) while lo < hi && hi - lo > SMALL_THRESHOLD pivot, j = swap ? partition!(v, lo, hi, o, t, rev) : partition!(t, lo, hi, o, v, rev) @@ -609,12 +601,12 @@ function sort!(v::AbstractVector, lo::Integer, hi::Integer, a::PartialQuickSort, # For QuickSort, a.lo === a.hi === missing, so the first two branches get skipped if !ismissing(a.lo) && j <= a.lo # Skip sorting the lower part swap && copyto!(v, lo, t, lo, j-lo) - rev && reverse_view!(v, lo, j-1) + rev && reverse!(v, lo, j-1) lo = j+1 rev = !rev elseif !ismissing(a.hi) && a.hi <= j # Skip sorting the upper part swap && copyto!(v, j+1, t, j+1, hi-j) - rev || reverse_view!(v, j+1, hi) + rev || reverse!(v, j+1, hi) hi = j-1 elseif j-lo < hi-j # Sort the lower part recursively because it is smaller. Recursing on the @@ -629,7 +621,7 @@ function sort!(v::AbstractVector, lo::Integer, hi::Integer, a::PartialQuickSort, end hi < lo && return v swap && copyto!(v, lo, t, lo, hi-lo+1) - rev && reverse_view!(v, lo, hi) + rev && reverse!(v, lo, hi) sort!(v, lo, hi, SMALL_ALGORITHM, o) end From d002d8812c9167b13cbb0fba135e3ae421162016 Mon Sep 17 00:00:00 2001 From: Lilith Hafner Date: Tue, 7 Jun 2022 06:52:56 -0400 Subject: [PATCH 04/30] Randomize pivot selection --- base/sort.jl | 33 ++++++--------------------------- stdlib/Random/src/Random.jl | 5 +++++ test/sorting.jl | 11 ----------- 3 files changed, 11 insertions(+), 38 deletions(-) diff --git a/base/sort.jl b/base/sort.jl index ff781aab2ccde..2482cfa2aca2c 100644 --- a/base/sort.jl +++ b/base/sort.jl @@ -531,33 +531,12 @@ function sort!(v::AbstractVector, lo::Integer, hi::Integer, ::InsertionSortAlg, return v end -# TODO Stable doesnt make sense because rev may be true!!!! -# return the index of the stable median of v[lo], v[mi], and v[hi] -# that is, compare the elements and break ties using the index +# select a pivot for QuickSort # -# for example, the stable median of [4, 4, 5] is the second -# element and the stable median of [4, 4, 3] is the first element -# -# TODO replace with rand(lo:hi) if Random is ever a part of Base -function select_pivot(v::AbstractVector, lo::Integer, hi::Integer, o::Ordering) - mi = midpoint(lo, hi) - @inbounds a, b, c = v[lo], v[mi], v[hi] - - if lt(o, b, a) - b, a = a, b - lo, mi = mi, lo - end - - # a is stably less than b - - if lt(o, c, a) - lo - elseif lt(o, c, b) - hi - else - mi - end -end +# This method is redefined to rand(lo:hi) in Random.jl +# We can't use rand here because it is not available in Core.Compiler and +# because rand is defined in the stdlib Random.jl after sorting it used in Base. +select_pivot(lo::Integer, hi::Integer) = midpoint(lo, hi) # select a pivot, partition v[lo:hi] according # to the pivot, and store the result in t[lo:hi]. @@ -565,7 +544,7 @@ end # returns (pivot, pivot_index) where pivot_index is the location the pivot # should end up, but does not set t[pivot_index] = pivot function partition!(t::AbstractVector, lo::Integer, hi::Integer, o::Ordering, v::AbstractVector, rev::Bool) - pivot_index = select_pivot(v, lo, hi, o) + pivot_index = select_pivot(lo, hi) trues = 0 @inbounds begin pivot = v[pivot_index] diff --git a/stdlib/Random/src/Random.jl b/stdlib/Random/src/Random.jl index 4eb7a418734c9..0d578ee795a9f 100644 --- a/stdlib/Random/src/Random.jl +++ b/stdlib/Random/src/Random.jl @@ -434,4 +434,9 @@ true """ seed!(rng::AbstractRNG, ::Nothing) = seed!(rng) +# Randomize quicksort pivot selection. This code is here because of bootstrapping: +# we need to sort things before we load this standard library. +# TODO move this into Sort.jl +Base.Sort.select_pivot(lo::Integer, hi::Integer) = rand(lo:hi) + end # module diff --git a/test/sorting.jl b/test/sorting.jl index 3c5751bcd69fb..97708ab0bc5d4 100644 --- a/test/sorting.jl +++ b/test/sorting.jl @@ -30,17 +30,6 @@ end @test -1 <= Base.Sort.midpoint(typemin(Int), typemax(Int)) <= 0 end -@testset "select_pivot" begin - @test Base.Sort.select_pivot([1, 1, 1], 1, 3, Forward) == 2 - @test Base.Sort.select_pivot([1, 3, 1], 1, 3, Reverse) == 1 - @test Base.Sort.select_pivot([2, 2, 3], 1, 3, Forward) == 2 - @test Base.Sort.select_pivot([2, 3, 1, 3, 1], 1, 5, Forward) == 5 - @test Base.Sort.select_pivot([2, 3, 1, 4, 2], 3, 5, Reverse) == 5 - @test Base.Sort.select_pivot([2, 2, 1], 1, 3, Forward) == 1 - @test Base.Sort.select_pivot([2, 2, 1], 1, 3, By(x -> 0)) == 2 - @test Base.Sort.select_pivot([3, 2, 1], 1, 3, Reverse) == 2 -end - @testset "sort" begin @test sort([2,3,1]) == [1,2,3] == sort([2,3,1]; order=Forward) @test sort([2,3,1], rev=true) == [3,2,1] == sort([2,3,1], order=Reverse) From c241adda5e58300dd65663cbe5f4fb0c7d41d8e1 Mon Sep 17 00:00:00 2001 From: Lilith Hafner Date: Tue, 7 Jun 2022 08:15:30 -0400 Subject: [PATCH 05/30] fix whitespace --- base/sort.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/base/sort.jl b/base/sort.jl index 2482cfa2aca2c..44b11a09aa840 100644 --- a/base/sort.jl +++ b/base/sort.jl @@ -6,7 +6,7 @@ import ..@__MODULE__, ..parentmodule const Base = parentmodule(@__MODULE__) using .Base.Order using .Base: length, first, last, axes, firstindex, lastindex, eltype, - similar, iterate, keytype, copymutable, fill, eachindex, zip, + similar, iterate, keytype, copymutable, fill, eachindex, zip, copyto!, reverse!, resize!, require_one_based_indexing, AbstractVector, Vector, AbstractRange, OrdinalRange, UnitRange, LinearIndices, identity, isless, min, max, extrema, sub_with_overflow, add_with_overflow, oneunit, @@ -477,7 +477,7 @@ Characteristics: * *not in-place* in memory. * *divide-and-conquer*: sort strategy similar to [`MergeSort`](@ref). * *good performance* for most large collections. - * *quadratic worst case runtime* in pathological cases + * *quadratic worst case runtime* in pathological cases (vanishingly rare for non-malicious input) """ const QuickSort = PartialQuickSort(missing, missing) From 93b80f6f87a53070f77f0d1b17ba99d04483493c Mon Sep 17 00:00:00 2001 From: Lilith Hafner Date: Thu, 30 Jun 2022 14:04:09 -0400 Subject: [PATCH 06/30] style --- base/sort.jl | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/base/sort.jl b/base/sort.jl index b23dc741f0d68..b50d13233b9af 100644 --- a/base/sort.jl +++ b/base/sort.jl @@ -535,7 +535,7 @@ end # # This method is redefined to rand(lo:hi) in Random.jl # We can't use rand here because it is not available in Core.Compiler and -# because rand is defined in the stdlib Random.jl after sorting it used in Base. +# because rand is defined in the stdlib Random.jl after sorting is used in Base. select_pivot(lo::Integer, hi::Integer) = midpoint(lo, hi) # select a pivot, partition v[lo:hi] according @@ -572,7 +572,8 @@ function partition!(t::AbstractVector, lo::Integer, hi::Integer, o::Ordering, v: pivot, lo-trues end -function sort!(v::AbstractVector, lo::Integer, hi::Integer, a::PartialQuickSort, o::Ordering, t::AbstractVector=similar(v), swap=false, rev=false) +function sort!(v::AbstractVector, lo::Integer, hi::Integer, a::PartialQuickSort, + o::Ordering, t::AbstractVector=similar(v), swap=false, rev=false) while lo < hi && hi - lo > SMALL_THRESHOLD pivot, j = swap ? partition!(v, lo, hi, o, t, rev) : partition!(t, lo, hi, o, v, rev) @inbounds v[j] = pivot @@ -717,7 +718,7 @@ end # This is an implementation of counting sort specialized for Bools. # Accepts unused buffer to avoid method ambiguity. function sort!(v::AbstractVector{B}, lo::Integer, hi::Integer, ::AdaptiveSortAlg, o::Ordering, - t::Union{AbstractVector{B}, Nothing}=nothing) where {B <: Bool} + t::Union{AbstractVector{B}, Nothing}=nothing) where {B <: Bool} first = lt(o, false, true) ? false : lt(o, true, false) ? true : return v count = 0 @inbounds for i in lo:hi @@ -742,7 +743,7 @@ function _extrema(v::AbstractVector, lo::Integer, hi::Integer, o::Ordering) mn, mx end function sort!(v::AbstractVector{T}, lo::Integer, hi::Integer, ::AdaptiveSortAlg, o::Ordering, - t::Union{AbstractVector{T}, Nothing}=nothing) where T + t::Union{AbstractVector{T}, Nothing}=nothing) where T # if the sorting task is not UIntMappable, then we can't radix sort or sort_int_range! # so we skip straight to the fallback algorithm which is comparison based. U = UIntMappable(eltype(v), o) @@ -857,12 +858,12 @@ defalg(v::AbstractArray{Missing}) = DEFAULT_UNSTABLE # for method disambiguation defalg(v::AbstractArray{Union{}}) = DEFAULT_UNSTABLE # for method disambiguation function sort!(v::AbstractVector{T}, alg::Algorithm, - order::Ordering, t::Union{AbstractVector{T}, Nothing}=nothing) where T + order::Ordering, t::Union{AbstractVector{T}, Nothing}=nothing) where T sort!(v, firstindex(v), lastindex(v), alg, order, t) end function sort!(v::AbstractVector{T}, lo::Integer, hi::Integer, alg::Algorithm, - order::Ordering, t::Union{AbstractVector{T}, Nothing}=nothing) where T + order::Ordering, t::Union{AbstractVector{T}, Nothing}=nothing) where T sort!(v, lo, hi, alg, order) end @@ -1546,7 +1547,7 @@ issignleft(o::ReverseOrdering, x::Floats) = lt(o, x, -zero(x)) issignleft(o::Perm, i::Integer) = issignleft(o.order, o.data[i]) function fpsort!(v::AbstractVector{T}, a::Algorithm, o::Ordering, - t::Union{AbstractVector{T}, Nothing}=nothing) where T + t::Union{AbstractVector{T}, Nothing}=nothing) where T # fpsort!'s optimizations speed up comparisons, of which there are O(nlogn). # The overhead is O(n). For n < 10, it's not worth it. length(v) < 10 && return sort!(v, firstindex(v), lastindex(v), SMALL_ALGORITHM, o, t) @@ -1564,12 +1565,13 @@ function fpsort!(v::AbstractVector{T}, a::Algorithm, o::Ordering, return v end + function sort!(v::FPSortable, a::Algorithm, o::DirectOrdering, - t::Union{FPSortable, Nothing}=nothing) + t::Union{FPSortable, Nothing}=nothing) fpsort!(v, a, o, t) end function sort!(v::AbstractVector{T}, a::Algorithm, o::Perm{<:DirectOrdering,<:FPSortable}, - t::Union{AbstractVector{T}, Nothing}=nothing) where T <: Union{Signed, Unsigned} + t::Union{AbstractVector{T}, Nothing}=nothing) where T <: Union{Signed, Unsigned} fpsort!(v, a, o, t) end From b78b99ab774d16b1739e5460c7bd8c3a77e3ec61 Mon Sep 17 00:00:00 2001 From: Lilith Hafner Date: Sun, 3 Jul 2022 20:39:20 -0400 Subject: [PATCH 07/30] Seed rng before generating precompile statements This fixes a hang where string interpolation in "precompile(Tuple{typeof(show), $IO, $T})\n" uses sorting which uses rand(lo:hi) which uses an itterated approach which runs forever because every random UInt64 is 0. TODO find a better place for this initialization to prevent this bug from cropping up again perhaps initialize the moment rand is defined (that may be hard). --- contrib/generate_precompile.jl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/contrib/generate_precompile.jl b/contrib/generate_precompile.jl index acd61be502465..d4f8e74f16c98 100644 --- a/contrib/generate_precompile.jl +++ b/contrib/generate_precompile.jl @@ -19,6 +19,12 @@ CTRL_C = '\x03' UP_ARROW = "\e[A" DOWN_ARROW = "\e[B" +task = current_task() +task.rngState0 = 0xb97cb0d700798446 +task.rngState1 = 0xfee2eb4d95cae2c1 +task.rngState2 = 0xa19fbca182930be0 +task.rngState3 = 0xc48c471658f3422e + hardcoded_precompile_statements = """ # used by Revise.jl precompile(Tuple{typeof(Base.parse_cache_header), String}) From e70ae49ac0536410f828095f5354164b4ef2e517 Mon Sep 17 00:00:00 2001 From: Lilith Orion Hafner <60898866+LilithHafner@users.noreply.github.com> Date: Sun, 3 Jul 2022 22:07:08 -0400 Subject: [PATCH 08/30] fix doctests (1/2) --- stdlib/Random/docs/src/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stdlib/Random/docs/src/index.md b/stdlib/Random/docs/src/index.md index 0f7636cf2444f..3f692bd305350 100644 --- a/stdlib/Random/docs/src/index.md +++ b/stdlib/Random/docs/src/index.md @@ -168,9 +168,9 @@ julia> rand(Die, 3) julia> a = Vector{Die}(undef, 3); rand!(a) 3-element Vector{Die}: + Die(9) Die(19) - Die(7) - Die(17) + Die(15) ``` #### A simple sampler without pre-computed data From 40342fe044b1009369a93ded581a20e11870f584 Mon Sep 17 00:00:00 2001 From: Lilith Hafner Date: Tue, 5 Jul 2022 23:24:08 -0400 Subject: [PATCH 09/30] revert e70ae49ac fix doctests (1/2) --- stdlib/Random/docs/src/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stdlib/Random/docs/src/index.md b/stdlib/Random/docs/src/index.md index 3f692bd305350..0f7636cf2444f 100644 --- a/stdlib/Random/docs/src/index.md +++ b/stdlib/Random/docs/src/index.md @@ -168,9 +168,9 @@ julia> rand(Die, 3) julia> a = Vector{Die}(undef, 3); rand!(a) 3-element Vector{Die}: - Die(9) Die(19) - Die(15) + Die(7) + Die(17) ``` #### A simple sampler without pre-computed data From c4104e19ecae0823daf3f2f27ef0e2ff530c9ce3 Mon Sep 17 00:00:00 2001 From: Lilith Hafner Date: Tue, 5 Jul 2022 23:25:13 -0400 Subject: [PATCH 10/30] fix doctests (2/2); hack names to not mutate rng state --- base/reflection.jl | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/base/reflection.jl b/base/reflection.jl index 644714c8440cb..4e05a44c9a360 100644 --- a/base/reflection.jl +++ b/base/reflection.jl @@ -99,8 +99,22 @@ since it is not idiomatic to explicitly export names from `Main`. See also: [`@locals`](@ref Base.@locals), [`@__MODULE__`](@ref). """ -names(m::Module; all::Bool = false, imported::Bool = false) = - sort!(ccall(:jl_module_names, Array{Symbol,1}, (Any, Cint, Cint), m, all, imported)) +function names(m::Module; all::Bool = false, imported::Bool = false) + # As of #45222, quicksort uses random pivot selection which mutates global rng state. + # This hack suppresses global rng mutation that Documenter.jl does not anticipate. + # TODO fix this downstream at https://github.com/JuliaDocs/Documenter.jl/issues/#### + preserve_rng() do + sort!(ccall(:jl_module_names, Array{Symbol,1}, (Any, Cint, Cint), m, all, imported)) + end +end + +function preserve_rng(f) + t = current_task() + x = t.rngState0, t.rngState1, t.rngState2, t.rngState3 + out = f() + t.rngState0, t.rngState1, t.rngState2, t.rngState3 = x + out +end isexported(m::Module, s::Symbol) = ccall(:jl_module_exports_p, Cint, (Any, Any), m, s) != 0 isdeprecated(m::Module, s::Symbol) = ccall(:jl_is_binding_deprecated, Cint, (Any, Any), m, s) != 0 From b1cb56d6a70615b5eb0e3c8319f2c1cfd9cbd27d Mon Sep 17 00:00:00 2001 From: Lilith Hafner Date: Wed, 6 Jul 2022 15:30:23 -0400 Subject: [PATCH 11/30] Use a better hack to fix doctests Rather than saving and restoring rng state, we simply use MergeSort. This is a bit unfortunate as it is the _only_ use of MergeSort in Base or stdlibs, but it is not a regression because we previously sorted symbols with MergeSort by default. --- base/reflection.jl | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/base/reflection.jl b/base/reflection.jl index 4e05a44c9a360..85b25a5bb0513 100644 --- a/base/reflection.jl +++ b/base/reflection.jl @@ -99,22 +99,12 @@ since it is not idiomatic to explicitly export names from `Main`. See also: [`@locals`](@ref Base.@locals), [`@__MODULE__`](@ref). """ -function names(m::Module; all::Bool = false, imported::Bool = false) - # As of #45222, quicksort uses random pivot selection which mutates global rng state. - # This hack suppresses global rng mutation that Documenter.jl does not anticipate. - # TODO fix this downstream at https://github.com/JuliaDocs/Documenter.jl/issues/#### - preserve_rng() do - sort!(ccall(:jl_module_names, Array{Symbol,1}, (Any, Cint, Cint), m, all, imported)) - end -end - -function preserve_rng(f) - t = current_task() - x = t.rngState0, t.rngState1, t.rngState2, t.rngState3 - out = f() - t.rngState0, t.rngState1, t.rngState2, t.rngState3 = x - out -end +names(m::Module; all::Bool = false, imported::Bool = false) = + # As of #45222, the default sorting algorithm for symbols mutates global rng state. + # Documenter.jl does not anticipate rng mutation here so we use MergeSort instead. + # TODO: fix this downstream at https://github.com/JuliaDocs/Documenter.jl/issues/#### + # and return to using the default sorting algorithm + sort!(ccall(:jl_module_names, Array{Symbol,1}, (Any, Cint, Cint), m, all, imported); alg=MergeSort) isexported(m::Module, s::Symbol) = ccall(:jl_module_exports_p, Cint, (Any, Any), m, s) != 0 isdeprecated(m::Module, s::Symbol) = ccall(:jl_is_binding_deprecated, Cint, (Any, Any), m, s) != 0 From f61070667f32bd3e96056e2a2356f1dbf2b04578 Mon Sep 17 00:00:00 2001 From: Lilith Orion Hafner Date: Wed, 6 Jul 2022 16:04:26 -0400 Subject: [PATCH 12/30] Avoid method overwrite warning during build --- stdlib/Random/src/Random.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/stdlib/Random/src/Random.jl b/stdlib/Random/src/Random.jl index c11d140c8126b..2ca7c67b58de1 100644 --- a/stdlib/Random/src/Random.jl +++ b/stdlib/Random/src/Random.jl @@ -437,6 +437,7 @@ seed!(rng::AbstractRNG, ::Nothing) = seed!(rng) # Randomize quicksort pivot selection. This code is here because of bootstrapping: # we need to sort things before we load this standard library. # TODO move this into Sort.jl +Base.delete_method(only(methods(Base.Sort.select_pivot))) Base.Sort.select_pivot(lo::Integer, hi::Integer) = rand(lo:hi) end # module From db2761d103a97f7d9611c5d739b0ed90c2844350 Mon Sep 17 00:00:00 2001 From: Lilith Hafner Date: Wed, 6 Jul 2022 16:21:40 -0400 Subject: [PATCH 13/30] introdce an easter egg --- contrib/generate_precompile.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contrib/generate_precompile.jl b/contrib/generate_precompile.jl index d4f8e74f16c98..9d88cadbe2aa5 100644 --- a/contrib/generate_precompile.jl +++ b/contrib/generate_precompile.jl @@ -20,10 +20,10 @@ UP_ARROW = "\e[A" DOWN_ARROW = "\e[B" task = current_task() -task.rngState0 = 0xb97cb0d700798446 -task.rngState1 = 0xfee2eb4d95cae2c1 -task.rngState2 = 0xa19fbca182930be0 -task.rngState3 = 0xc48c471658f3422e +task.rngState0 = 0x33680cab0d0d3ae2 +task.rngState1 = 0x13d8eff5fab25661 +task.rngState2 = 0x631fbafa04b7fcf5 +task.rngState3 = 0xa4239a7dbd0a1a97 hardcoded_precompile_statements = """ # used by Revise.jl From a408952f1231d543965fe111a9feddb9c7becdb5 Mon Sep 17 00:00:00 2001 From: Lilith Hafner Date: Thu, 7 Jul 2022 15:46:40 -0400 Subject: [PATCH 14/30] Use an even better hack to avoid breaking docstrings It's so good it's not even a hack. The problems come from an unfortunately placed call to sort, but we don't actually need to sort in those cases. --- base/reflection.jl | 8 +++----- base/show.jl | 4 ++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/base/reflection.jl b/base/reflection.jl index 85b25a5bb0513..d0def5c4ee8d4 100644 --- a/base/reflection.jl +++ b/base/reflection.jl @@ -100,11 +100,9 @@ since it is not idiomatic to explicitly export names from `Main`. See also: [`@locals`](@ref Base.@locals), [`@__MODULE__`](@ref). """ names(m::Module; all::Bool = false, imported::Bool = false) = - # As of #45222, the default sorting algorithm for symbols mutates global rng state. - # Documenter.jl does not anticipate rng mutation here so we use MergeSort instead. - # TODO: fix this downstream at https://github.com/JuliaDocs/Documenter.jl/issues/#### - # and return to using the default sorting algorithm - sort!(ccall(:jl_module_names, Array{Symbol,1}, (Any, Cint, Cint), m, all, imported); alg=MergeSort) + sort!(unsorted_names(m; all, imported)) +unsorted_names(m::Module; all::Bool = false, imported::Bool = false) = + ccall(:jl_module_names, Array{Symbol,1}, (Any, Cint, Cint), m, all, imported) isexported(m::Module, s::Symbol) = ccall(:jl_module_exports_p, Cint, (Any, Any), m, s) != 0 isdeprecated(m::Module, s::Symbol) = ccall(:jl_is_binding_deprecated, Cint, (Any, Any), m, s) != 0 diff --git a/base/show.jl b/base/show.jl index 9841d34efe88b..a0ab37a7559a5 100644 --- a/base/show.jl +++ b/base/show.jl @@ -597,7 +597,7 @@ function make_typealias(@nospecialize(x::Type)) end x isa UnionAll && push!(xenv, x) for mod in mods - for name in names(mod) + for name in unsorted_names(mod) if isdefined(mod, name) && !isdeprecated(mod, name) && isconst(mod, name) alias = getfield(mod, name) if alias isa Type && !has_free_typevars(alias) && !print_without_params(alias) && x <: alias @@ -801,7 +801,7 @@ function make_typealiases(@nospecialize(x::Type)) end x isa UnionAll && push!(xenv, x) for mod in mods - for name in names(mod) + for name in unsorted_names(mod) if isdefined(mod, name) && !isdeprecated(mod, name) && isconst(mod, name) alias = getfield(mod, name) if alias isa Type && !has_free_typevars(alias) && !print_without_params(alias) && !(alias <: Tuple) From a3bf888b7b577b341f533d30b9b9027d048eedc9 Mon Sep 17 00:00:00 2001 From: Lilith Hafner Date: Tue, 19 Jul 2022 11:21:13 -0400 Subject: [PATCH 15/30] Add presorted check --- base/sort.jl | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/base/sort.jl b/base/sort.jl index 8ccb71ecc3ae4..cb77760c3e984 100644 --- a/base/sort.jl +++ b/base/sort.jl @@ -574,7 +574,19 @@ function partition!(t::AbstractVector, lo::Integer, hi::Integer, o::Ordering, v: end function sort!(v::AbstractVector, lo::Integer, hi::Integer, a::PartialQuickSort, - o::Ordering, t::AbstractVector=similar(v), swap=false, rev=false) + o::Ordering, t::AbstractVector=similar(v), swap=false, rev=false; + check_presorted=true) + + if check_presorted && !rev && !swap + lo2 = ismissing(a.lo) ? lo : a.lo + hi2 = ismissing(a.hi) ? hi : a.hi + if _issorted(v, lo2, hi2, o) + return v + elseif _issorted(v, lo2, hi2, Reverse(o)) + return reverse!(v, lo2, hi2) + end + end + while lo < hi && hi - lo > SMALL_THRESHOLD pivot, j = swap ? partition!(v, lo, hi, o, t, rev) : partition!(t, lo, hi, o, v, rev) @inbounds v[j] = pivot @@ -593,11 +605,11 @@ function sort!(v::AbstractVector, lo::Integer, hi::Integer, a::PartialQuickSort, elseif j-lo < hi-j # Sort the lower part recursively because it is smaller. Recursing on the # smaller part guarantees O(log(n)) stack space even on pathological inputs. - sort!(v, lo, j-1, a, o, t, swap, rev) + sort!(v, lo, j-1, a, o, t, swap, rev; check_presorted=false) lo = j+1 rev = !rev else # Sort the higher part recursively - sort!(v, j+1, hi, a, o, t, swap, !rev) + sort!(v, j+1, hi, a, o, t, swap, !rev; check_presorted=false) hi = j-1 end end @@ -790,7 +802,7 @@ function sort!(v::AbstractVector{T}, lo::Integer, hi::Integer, ::AdaptiveSortAlg return sort_int_range!(v, Int(v_range+1), v_min, o === Forward ? identity : reverse, lo, hi) end end - return sort!(v, lo, hi, QuickSort, o) + return sort!(v, lo, hi, QuickSort, o; check_presorted=false) end v_min, v_max = _extrema(v, lo, hi, o) @@ -825,7 +837,11 @@ function sort!(v::AbstractVector{T}, lo::Integer, hi::Integer, ::AdaptiveSortAlg # lenm1 < 3bits if lenm1 < 3bits # at lenm1 = 64*3-1, QuickSort is about 20% faster than InsertionSort. - return sort!(v, lo, hi, lenm1 > 120 ? QuickSort : SMALL_ALGORITHM, o) + return if lenm1 > 120 + sort!(v, lo, hi, QuickSort, o; check_presorted=false) + else + sort!(v, lo, hi, SMALL_ALGORITHM, o) + end end # At this point, we are committed to radix sort. From d09dc37e84669b0a8eb95007b957756b93054762 Mon Sep 17 00:00:00 2001 From: Lilith Hafner Date: Tue, 19 Jul 2022 11:53:42 -0400 Subject: [PATCH 16/30] tweak threshold because quicksort is a bit better now --- base/sort.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/base/sort.jl b/base/sort.jl index cb77760c3e984..ca177e572f7c3 100644 --- a/base/sort.jl +++ b/base/sort.jl @@ -836,8 +836,8 @@ function sort!(v::AbstractVector{T}, lo::Integer, hi::Integer, ::AdaptiveSortAlg # lenm1^2 < 3 * bits * lenm1 # lenm1 < 3bits if lenm1 < 3bits - # at lenm1 = 64*3-1, QuickSort is about 20% faster than InsertionSort. - return if lenm1 > 120 + # at lenm1 = 64*3-1, QuickSort is about 40% faster than InsertionSort. + return if lenm1 > 80 sort!(v, lo, hi, QuickSort, o; check_presorted=false) else sort!(v, lo, hi, SMALL_ALGORITHM, o) From 1cb2c2ba1ed706a4c6f895971e590aca5b31ded2 Mon Sep 17 00:00:00 2001 From: Lilith Hafner Date: Tue, 19 Jul 2022 12:21:04 -0400 Subject: [PATCH 17/30] fixup --- base/sort.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/sort.jl b/base/sort.jl index ca177e572f7c3..86bcf96e824c4 100644 --- a/base/sort.jl +++ b/base/sort.jl @@ -582,7 +582,7 @@ function sort!(v::AbstractVector, lo::Integer, hi::Integer, a::PartialQuickSort, hi2 = ismissing(a.hi) ? hi : a.hi if _issorted(v, lo2, hi2, o) return v - elseif _issorted(v, lo2, hi2, Reverse(o)) + elseif _issorted(v, lo2, hi2, ReverseOrdering(o)) return reverse!(v, lo2, hi2) end end From 9942028daf8856d53f9572f3a7efd7a0263c9537 Mon Sep 17 00:00:00 2001 From: Lilith Hafner Date: Tue, 19 Jul 2022 18:24:08 -0400 Subject: [PATCH 18/30] test #32675 --- test/sorting.jl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/sorting.jl b/test/sorting.jl index 987255dad3c7e..eac0b80f36681 100644 --- a/test/sorting.jl +++ b/test/sorting.jl @@ -723,6 +723,14 @@ end end @test partialsort(v, 172, lt = (x,y) -> rand([false, true])) ∈ 1:5 @test all(partialsort(v, 315:415, lt = (x,y) -> rand([false, true])) .∈ (1:5,)) + + # issue #32675 + k = [38, 18, 38, 38, 3, 37, 26, 26, 6, 29, 38, 36, 38, 1, 38, 36, 38, 38, 38, 36, 36, + 36, 28, 34, 35, 38, 25, 20, 38, 1, 1, 5, 38, 38, 3, 34, 16, 38, 4, 10, 35, 37, 38, + 38, 2, 38, 25, 35, 38, 1, 35, 36, 20, 33, 36, 18, 38, 1, 24, 4, 38, 18, 12, 38, 34, + 35, 36, 38, 26, 31, 36, 38, 38, 30, 36, 35, 35, 7, 22, 35, 38, 35, 30, 21, 37] + idx = sortperm(k; lt=!isless) + @test issorted(k[idx], rev=true) end # This testset is at the end of the file because it is slow From 822f089826ba86b887a5b84222ab07971d270235 Mon Sep 17 00:00:00 2001 From: Lilith Hafner Date: Tue, 19 Jul 2022 19:34:19 -0400 Subject: [PATCH 19/30] tighten presorted check on partial sort --- base/sort.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/base/sort.jl b/base/sort.jl index 86bcf96e824c4..88e6649cd82b4 100644 --- a/base/sort.jl +++ b/base/sort.jl @@ -578,12 +578,12 @@ function sort!(v::AbstractVector, lo::Integer, hi::Integer, a::PartialQuickSort, check_presorted=true) if check_presorted && !rev && !swap - lo2 = ismissing(a.lo) ? lo : a.lo - hi2 = ismissing(a.hi) ? hi : a.hi - if _issorted(v, lo2, hi2, o) + # Even if we are only sorting a short region, we can only short-circuit if the whole + # vector is presorted. A weaker condition is possible, but unlikely to be useful. + if _issorted(v, lo, hi, o) return v - elseif _issorted(v, lo2, hi2, ReverseOrdering(o)) - return reverse!(v, lo2, hi2) + elseif _issorted(v, lo, hi, ReverseOrdering(o)) + return reverse!(v, lo, hi) end end From 57e4d17dcb7b5e75a7bc6c62c913b74032af9dfc Mon Sep 17 00:00:00 2001 From: Lilith Hafner Date: Wed, 20 Jul 2022 15:46:15 -0400 Subject: [PATCH 20/30] tighten reverse-sorted check further to ensure stability --- base/sort.jl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/base/sort.jl b/base/sort.jl index 2ea88ffd904e2..cc06f23aac9e6 100644 --- a/base/sort.jl +++ b/base/sort.jl @@ -582,7 +582,8 @@ function sort!(v::AbstractVector, lo::Integer, hi::Integer, a::PartialQuickSort, # vector is presorted. A weaker condition is possible, but unlikely to be useful. if _issorted(v, lo, hi, o) return v - elseif _issorted(v, lo, hi, ReverseOrdering(o)) + elseif _issorted(v, lo, hi, Lt((x, y) -> !lt(o, x, y))) + # Reverse only if necessary. A weaker condition would violate stability. return reverse!(v, lo, hi) end end @@ -784,6 +785,8 @@ function sort!(v::AbstractVector{T}, lo::Integer, hi::Integer, ::AdaptiveSortAlg # For large arrays, a reverse-sorted check is essentially free (overhead < 1%) if lenm1 >= 500 && _issorted(v, lo, hi, ReverseOrdering(o)) + # If reversing is valid, do so. This does not violate stability + # because being UIntMappable implies a linear order. reverse!(v, lo, hi) return v end From 9cf4f730454f6d19d464e78ee165fd6c2d64dede Mon Sep 17 00:00:00 2001 From: Lilith Hafner Date: Wed, 20 Jul 2022 16:14:19 -0400 Subject: [PATCH 21/30] test stable reverse short circuit --- test/sorting.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/sorting.jl b/test/sorting.jl index 13bda0dce7b3a..0e45bd876fdcd 100644 --- a/test/sorting.jl +++ b/test/sorting.jl @@ -343,6 +343,10 @@ end @test s == si invpermute!(s, p) @test s == v + + # Ensure stability, even with reverse short circuit + @test all(sort!(Real[fill(2.0, 15); fill(2, 15); fill(1.0, 15); fill(1, 15)]) + .=== Real[fill(1.0, 15); fill(1, 15); fill(2.0, 15); fill(2, 15)]) end # unstable algorithms From 52f5dfe3e1403e75b51f219f34533adc75655d87 Mon Sep 17 00:00:00 2001 From: Lilith Hafner Date: Wed, 20 Jul 2022 16:15:06 -0400 Subject: [PATCH 22/30] organize sorting tests sligthly & test that PartialQuickSort is stable --- test/sorting.jl | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/test/sorting.jl b/test/sorting.jl index 0e45bd876fdcd..3ad9f2b361e40 100644 --- a/test/sorting.jl +++ b/test/sorting.jl @@ -128,9 +128,11 @@ Base.step(r::ConstantRange) = 0 @test searchsortedlast(r, 1.0, Forward) == 5 @test searchsortedlast(r, 1, Forward) == 5 @test searchsortedlast(r, UInt(1), Forward) == 5 +end +@testset "Each sorting algorithm individually" begin a = rand(1:10000, 1000) - for alg in [InsertionSort, MergeSort, QuickSort, Base.DEFAULT_STABLE] + for alg in [InsertionSort, MergeSort, QuickSort, Base.DEFAULT_STABLE, Base.DEFAULT_UNSTABLE] b = sort(a, alg=alg) @test issorted(b) @@ -195,18 +197,16 @@ Base.step(r::ConstantRange) = 0 @test b == c end - @testset "unstable algorithms" begin - for alg in [QuickSort, Base.DEFAULT_UNSTABLE] - b = sort(a, alg=alg) - @test issorted(b) - @test last(b) == last(sort(a, alg=PartialQuickSort(length(a)))) - b = sort(a, alg=alg, rev=true) - @test issorted(b, rev=true) - @test last(b) == last(sort(a, alg=PartialQuickSort(length(a)), rev=true)) - b = sort(a, alg=alg, by=x->1/x) - @test issorted(b, by=x->1/x) - @test last(b) == last(sort(a, alg=PartialQuickSort(length(a)), by=x->1/x)) - end + @testset "PartialQuickSort" begin + b = sort(a) + @test issorted(b) + @test last(b) == last(sort(a, alg=PartialQuickSort(length(a)))) + b = sort(a, rev=true) + @test issorted(b, rev=true) + @test last(b) == last(sort(a, alg=PartialQuickSort(length(a)), rev=true)) + b = sort(a, by=x->1/x) + @test issorted(b, by=x->1/x) + @test last(b) == last(sort(a, alg=PartialQuickSort(length(a)), by=x->1/x)) end end @testset "insorted" begin @@ -333,7 +333,7 @@ end @test c == v # stable algorithms - for alg in [MergeSort, QuickSort, Base.DEFAULT_STABLE] + for alg in [MergeSort, QuickSort, PartialQuickSort(1:n), Base.DEFAULT_STABLE] p = sortperm(v, alg=alg, rev=rev) p2 = sortperm(float(v), alg=alg, rev=rev) @test p == p2 From 4b05e9dc85b9610bc520c0cc372fddabd59998ea Mon Sep 17 00:00:00 2001 From: Lilith Orion Hafner Date: Sun, 24 Jul 2022 13:50:43 -0400 Subject: [PATCH 23/30] Clarify comment --- base/sort.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/sort.jl b/base/sort.jl index cc06f23aac9e6..82e62e4dc77e7 100644 --- a/base/sort.jl +++ b/base/sort.jl @@ -583,7 +583,7 @@ function sort!(v::AbstractVector, lo::Integer, hi::Integer, a::PartialQuickSort, if _issorted(v, lo, hi, o) return v elseif _issorted(v, lo, hi, Lt((x, y) -> !lt(o, x, y))) - # Reverse only if necessary. A weaker condition would violate stability. + # Reverse only if necessary. Using issorted(..., Reverse(o)) would violate stability. return reverse!(v, lo, hi) end end From 76c606e64578fef0490d4302c27e7b22346d4c62 Mon Sep 17 00:00:00 2001 From: Lilith Hafner Date: Sun, 31 Jul 2022 06:51:22 -0500 Subject: [PATCH 24/30] remove rng initialization and the unnecessary sort that requires it --- contrib/generate_precompile.jl | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/contrib/generate_precompile.jl b/contrib/generate_precompile.jl index 9d88cadbe2aa5..2afc7c944ce36 100644 --- a/contrib/generate_precompile.jl +++ b/contrib/generate_precompile.jl @@ -19,12 +19,6 @@ CTRL_C = '\x03' UP_ARROW = "\e[A" DOWN_ARROW = "\e[B" -task = current_task() -task.rngState0 = 0x33680cab0d0d3ae2 -task.rngState1 = 0x13d8eff5fab25661 -task.rngState2 = 0x631fbafa04b7fcf5 -task.rngState3 = 0xa4239a7dbd0a1a97 - hardcoded_precompile_statements = """ # used by Revise.jl precompile(Tuple{typeof(Base.parse_cache_header), String}) @@ -378,9 +372,9 @@ function generate_precompile_statements() end end - # Execute the collected precompile statements + # Execute the precompile statements n_succeeded = 0 - include_time = @elapsed for statement in sort!(collect(statements)) + include_time = @elapsed for statement in statements # println(statement) # XXX: skip some that are broken. these are caused by issue #39902 occursin("Tuple{Artifacts.var\"#@artifact_str\", LineNumberNode, Module, Any, Any}", statement) && continue From 5b82421445222874b49f784af00dd9861f4b7b51 Mon Sep 17 00:00:00 2001 From: Lilith Hafner Date: Sun, 14 Aug 2022 15:41:05 -0500 Subject: [PATCH 25/30] fix merge conflict with 46278 --- base/sysimg.jl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/base/sysimg.jl b/base/sysimg.jl index 79f01a0e9a1d2..aa10f32fb2fe2 100644 --- a/base/sysimg.jl +++ b/base/sysimg.jl @@ -14,6 +14,14 @@ pushfirst!(Base._included_files, (@__MODULE__, joinpath(@__DIR__, "sysimg.jl"))) if Base.is_primary_base_module # load some stdlib packages but don't put their names in Main let + # Loading here does not call __init__(). This leads to uninitialized RNG + # state which causes rand(::UnitRange{Int}) to hang. This is a workaround: + task = current_task() + task.rngState0 = 0x5156087469e170ab + task.rngState1 = 0x7431eaead385992c + task.rngState2 = 0x503e1d32781c2608 + task.rngState3 = 0x3a77f7189200c20b + # set up depot & load paths to be able to find stdlib packages push!(empty!(LOAD_PATH), "@stdlib") Base.append_default_depot_path!(DEPOT_PATH) From c11a41bae7d312c3781f7039062b3043aad762aa Mon Sep 17 00:00:00 2001 From: Lilith Hafner Date: Sat, 27 Aug 2022 11:31:13 -0500 Subject: [PATCH 26/30] Update docstring thanks @oscardssmith --- base/sort.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/sort.jl b/base/sort.jl index dda28fa8015bb..728fa468240c6 100644 --- a/base/sort.jl +++ b/base/sort.jl @@ -475,7 +475,7 @@ Characteristics: (e.g. "a" and "A" in a sort of letters which ignores case). * *not in-place* in memory. * *divide-and-conquer*: sort strategy similar to [`MergeSort`](@ref). - * *good performance* for most large collections. + * *good performance* for almost all large collections. * *quadratic worst case runtime* in pathological cases (vanishingly rare for non-malicious input) """ From 64b57efcdc085365a7bdf1ff8b2824b80df41227 Mon Sep 17 00:00:00 2001 From: Lilith Hafner Date: Tue, 11 Oct 2022 09:05:32 +0600 Subject: [PATCH 27/30] better deterministic pivot selction --- base/sort.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/sort.jl b/base/sort.jl index 77d1c159eefff..daff45162f8f7 100644 --- a/base/sort.jl +++ b/base/sort.jl @@ -542,7 +542,7 @@ end # This method is redefined to rand(lo:hi) in Random.jl # We can't use rand here because it is not available in Core.Compiler and # because rand is defined in the stdlib Random.jl after sorting is used in Base. -select_pivot(lo::Integer, hi::Integer) = midpoint(lo, hi) +select_pivot(lo::Integer, hi::Integer) = typeof(hi-lo)(hash((lo, hi)) % (hi-lo+1)) + lo # select a pivot, partition v[lo:hi] according # to the pivot, and store the result in t[lo:hi]. From 7560e6189d8c620bc69d1f10490de2302b86167b Mon Sep 17 00:00:00 2001 From: Lilith Hafner Date: Tue, 11 Oct 2022 18:16:50 +0600 Subject: [PATCH 28/30] fixup. using Base: hash --- base/sort.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/sort.jl b/base/sort.jl index daff45162f8f7..50737dbf494bf 100644 --- a/base/sort.jl +++ b/base/sort.jl @@ -14,7 +14,7 @@ using .Base: length, first, last, axes, firstindex, lastindex, eltype, reinterpret, signed, unsigned, Signed, Unsigned, typemin, Type, BitSigned, Val, Missing, missing, ismissing, @eval, @inbounds, @inline, @noinline, (:), >, <, <=, >=, ==, !=, ===, |, +, -, *, !, <<, >>, &, >>>, !==, div, xor, - midpoint, @boundscheck, checkbounds + midpoint, @boundscheck, checkbounds, hash import .Base: sort, From 24fd62b27a54d1a47d6f2be6ce832babf2e1808c Mon Sep 17 00:00:00 2001 From: Lilith Orion Hafner Date: Wed, 12 Oct 2022 18:30:21 +0600 Subject: [PATCH 29/30] Optimize deterministic select_pivot Co-authored-by: Petr Vana --- base/sort.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/sort.jl b/base/sort.jl index 50737dbf494bf..228c517ff00b8 100644 --- a/base/sort.jl +++ b/base/sort.jl @@ -542,7 +542,7 @@ end # This method is redefined to rand(lo:hi) in Random.jl # We can't use rand here because it is not available in Core.Compiler and # because rand is defined in the stdlib Random.jl after sorting is used in Base. -select_pivot(lo::Integer, hi::Integer) = typeof(hi-lo)(hash((lo, hi)) % (hi-lo+1)) + lo +select_pivot(lo::Integer, hi::Integer) = typeof(hi-lo)(hash(lo) % (hi-lo+1)) + lo # select a pivot, partition v[lo:hi] according # to the pivot, and store the result in t[lo:hi]. From 2272c28f7fb72655e5e8857ff77bb961765baf8b Mon Sep 17 00:00:00 2001 From: Lilith Hafner Date: Fri, 14 Oct 2022 18:19:11 +0600 Subject: [PATCH 30/30] update radix sort heuristic because quicksort is faster and the primary competition --- base/sort.jl | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/base/sort.jl b/base/sort.jl index 228c517ff00b8..a986ac827ef6e 100644 --- a/base/sort.jl +++ b/base/sort.jl @@ -836,15 +836,10 @@ function sort!(v::AbstractVector{T}, lo::Integer, hi::Integer, ::AdaptiveSortAlg # where we only need to radix over the last few bits (5, in the example). bits = unsigned(8sizeof(u_range) - leading_zeros(u_range)) - # radix sort runs in O(bits * lenm1), insertion sort runs in O(lenm1^2). Radix sort - # has a constant factor that is three times higher, so radix runtime is 3bits * lenm1 - # and insertion runtime is lenm1^2. Empirically, insertion is faster than radix iff - # lenm1 < 3bits. - # Insertion < Radix - # lenm1^2 < 3 * bits * lenm1 - # lenm1 < 3bits - if lenm1 < 3bits - # at lenm1 = 64*3-1, QuickSort is about 40% faster than InsertionSort. + # radix sort runs in O(bits * lenm1), quick sort runs in O(lenm1 * log(lenm1)). + # dividing both sides by lenm1 and introducing empirical constant factors yields + # the following heuristic for when QuickSort is faster than RadixSort + if 22log(lenm1) < bits + 70 return if lenm1 > 80 sort!(v, lo, hi, QuickSort, o; check_presorted=false) else