Skip to content

Commit a738d81

Browse files
authored
[libc] Improve qsort (with build fix) (llvm#121482)
1 parent 7a76110 commit a738d81

File tree

17 files changed

+569
-325
lines changed

17 files changed

+569
-325
lines changed

libc/fuzzing/stdlib/CMakeLists.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
add_libc_fuzzer(
2-
qsort_fuzz
2+
quick_sort_fuzz
33
SRCS
4-
qsort_fuzz.cpp
4+
quick_sort_fuzz.cpp
55
DEPENDS
6-
libc.src.stdlib.qsort
6+
libc.src.stdlib.qsort_util
77
)
88

99
add_libc_fuzzer(

libc/fuzzing/stdlib/heap_sort_fuzz.cpp

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,10 @@
1010
///
1111
//===----------------------------------------------------------------------===//
1212

13-
#include "src/stdlib/heap_sort.h"
13+
#include "src/stdlib/qsort_util.h"
1414
#include <stdint.h>
1515

16-
static int int_compare(const void *l, const void *r) {
17-
int li = *reinterpret_cast<const int *>(l);
18-
int ri = *reinterpret_cast<const int *>(r);
19-
if (li == ri)
20-
return 0;
21-
if (li > ri)
22-
return 1;
23-
return -1;
24-
}
25-
2616
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
27-
2817
const size_t array_size = size / sizeof(int);
2918
if (array_size == 0)
3019
return 0;
@@ -34,14 +23,22 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
3423
for (size_t i = 0; i < array_size; ++i)
3524
array[i] = data_as_int[i];
3625

37-
auto arr = LIBC_NAMESPACE::internal::Array(
38-
reinterpret_cast<uint8_t *>(array), array_size, sizeof(int), int_compare);
26+
const auto is_less = [](const void *a_ptr,
27+
const void *b_ptr) noexcept -> bool {
28+
const int &a = *static_cast<const int *>(a_ptr);
29+
const int &b = *static_cast<const int *>(b_ptr);
30+
31+
return a < b;
32+
};
3933

40-
LIBC_NAMESPACE::internal::heap_sort(arr);
34+
constexpr bool USE_QUICKSORT = false;
35+
LIBC_NAMESPACE::internal::unstable_sort_impl<USE_QUICKSORT>(
36+
array, array_size, sizeof(int), is_less);
4137

42-
for (size_t i = 0; i < array_size - 1; ++i)
38+
for (size_t i = 0; i < array_size - 1; ++i) {
4339
if (array[i] > array[i + 1])
4440
__builtin_trap();
41+
}
4542

4643
delete[] array;
4744
return 0;

libc/fuzzing/stdlib/qsort_fuzz.cpp renamed to libc/fuzzing/stdlib/quick_sort_fuzz.cpp

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,18 @@
1-
//===-- qsort_fuzz.cpp ----------------------------------------------------===//
1+
//===-- quick_sort_fuzz.cpp------------------------------------------------===//
22
//
33
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
44
// See https://llvm.org/LICENSE.txt for license information.
55
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
66
//
77
//===----------------------------------------------------------------------===//
88
///
9-
/// Fuzzing test for llvm-libc qsort implementation.
9+
/// Fuzzing test for llvm-libc quick_sort implementation.
1010
///
1111
//===----------------------------------------------------------------------===//
1212

13-
#include "src/stdlib/qsort.h"
13+
#include "src/stdlib/qsort_util.h"
1414
#include <stdint.h>
1515

16-
static int int_compare(const void *l, const void *r) {
17-
int li = *reinterpret_cast<const int *>(l);
18-
int ri = *reinterpret_cast<const int *>(r);
19-
if (li == ri)
20-
return 0;
21-
else if (li > ri)
22-
return 1;
23-
else
24-
return -1;
25-
}
26-
2716
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
2817
const size_t array_size = size / sizeof(int);
2918
if (array_size == 0)
@@ -34,7 +23,17 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
3423
for (size_t i = 0; i < array_size; ++i)
3524
array[i] = data_as_int[i];
3625

37-
LIBC_NAMESPACE::qsort(array, array_size, sizeof(int), int_compare);
26+
const auto is_less = [](const void *a_ptr,
27+
const void *b_ptr) noexcept -> bool {
28+
const int &a = *static_cast<const int *>(a_ptr);
29+
const int &b = *static_cast<const int *>(b_ptr);
30+
31+
return a < b;
32+
};
33+
34+
constexpr bool USE_QUICKSORT = true;
35+
LIBC_NAMESPACE::internal::unstable_sort_impl<USE_QUICKSORT>(
36+
array, array_size, sizeof(int), is_less);
3837

3938
for (size_t i = 0; i < array_size - 1; ++i) {
4039
if (array[i] > array[i + 1])

libc/src/stdlib/heap_sort.h

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@ namespace internal {
1818
// A simple in-place heapsort implementation.
1919
// Follow the implementation in https://en.wikipedia.org/wiki/Heapsort.
2020

21-
LIBC_INLINE void heap_sort(const Array &array) {
22-
size_t end = array.size();
21+
template <typename A, typename F>
22+
LIBC_INLINE void heap_sort(const A &array, const F &is_less) {
23+
size_t end = array.len();
2324
size_t start = end / 2;
2425

25-
auto left_child = [](size_t i) -> size_t { return 2 * i + 1; };
26+
const auto left_child = [](size_t i) -> size_t { return 2 * i + 1; };
2627

2728
while (end > 1) {
2829
if (start > 0) {
@@ -40,12 +41,11 @@ LIBC_INLINE void heap_sort(const Array &array) {
4041
while (left_child(root) < end) {
4142
size_t child = left_child(root);
4243
// If there are two children, set child to the greater.
43-
if (child + 1 < end &&
44-
array.elem_compare(child, array.get(child + 1)) < 0)
44+
if ((child + 1 < end) && is_less(array.get(child), array.get(child + 1)))
4545
++child;
4646

4747
// If the root is less than the greater child
48-
if (array.elem_compare(root, array.get(child)) >= 0)
48+
if (!is_less(array.get(root), array.get(child)))
4949
break;
5050

5151
// Swap the root with the greater child and continue sifting down.

libc/src/stdlib/qsort.cpp

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,12 @@ namespace LIBC_NAMESPACE_DECL {
1818
LLVM_LIBC_FUNCTION(void, qsort,
1919
(void *array, size_t array_size, size_t elem_size,
2020
int (*compare)(const void *, const void *))) {
21-
if (array == nullptr || array_size == 0 || elem_size == 0)
22-
return;
23-
internal::Comparator c(compare);
2421

25-
auto arr = internal::Array(reinterpret_cast<uint8_t *>(array), array_size,
26-
elem_size, c);
22+
const auto is_less = [compare](const void *a, const void *b) -> bool {
23+
return compare(a, b) < 0;
24+
};
2725

28-
internal::sort(arr);
26+
internal::unstable_sort(array, array_size, elem_size, is_less);
2927
}
3028

3129
} // namespace LIBC_NAMESPACE_DECL

libc/src/stdlib/qsort_data.h

Lines changed: 101 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -17,91 +17,122 @@
1717
namespace LIBC_NAMESPACE_DECL {
1818
namespace internal {
1919

20-
using Compare = int(const void *, const void *);
21-
using CompareWithState = int(const void *, const void *, void *);
22-
23-
enum class CompType { COMPARE, COMPARE_WITH_STATE };
24-
25-
struct Comparator {
26-
union {
27-
Compare *comp_func;
28-
CompareWithState *comp_func_r;
29-
};
30-
const CompType comp_type;
31-
32-
void *arg;
33-
34-
Comparator(Compare *func)
35-
: comp_func(func), comp_type(CompType::COMPARE), arg(nullptr) {}
36-
37-
Comparator(CompareWithState *func, void *arg_val)
38-
: comp_func_r(func), comp_type(CompType::COMPARE_WITH_STATE),
39-
arg(arg_val) {}
40-
41-
#if defined(__clang__)
42-
// Recent upstream changes to -fsanitize=function find more instances of
43-
// function type mismatches. One case is with the comparator passed to this
44-
// class. Libraries will tend to pass comparators that take pointers to
45-
// varying types while this comparator expects to accept const void pointers.
46-
// Ideally those tools would pass a function that strictly accepts const
47-
// void*s to avoid UB, or would use qsort_r to pass their own comparator.
48-
[[clang::no_sanitize("function")]]
49-
#endif
50-
int comp_vals(const void *a, const void *b) const {
51-
if (comp_type == CompType::COMPARE) {
52-
return comp_func(a, b);
53-
} else {
54-
return comp_func_r(a, b, arg);
20+
class ArrayGenericSize {
21+
cpp::byte *array_base;
22+
size_t array_len;
23+
size_t elem_size;
24+
25+
LIBC_INLINE cpp::byte *get_internal(size_t i) const {
26+
return array_base + (i * elem_size);
27+
}
28+
29+
public:
30+
LIBC_INLINE ArrayGenericSize(void *a, size_t s, size_t e)
31+
: array_base(reinterpret_cast<cpp::byte *>(a)), array_len(s),
32+
elem_size(e) {}
33+
34+
static constexpr bool has_fixed_size() { return false; }
35+
36+
LIBC_INLINE void *get(size_t i) const { return get_internal(i); }
37+
38+
LIBC_INLINE void swap(size_t i, size_t j) const {
39+
// It's possible to use 8 byte blocks with `uint64_t`, but that
40+
// generates more machine code as the remainder loop gets
41+
// unrolled, plus 4 byte operations are more likely to be
42+
// efficient on a wider variety of hardware. On x86 LLVM tends
43+
// to unroll the block loop again into 2 16 byte swaps per
44+
// iteration which is another reason that 4 byte blocks yields
45+
// good performance even for big types.
46+
using block_t = uint32_t;
47+
constexpr size_t BLOCK_SIZE = sizeof(block_t);
48+
49+
alignas(block_t) cpp::byte tmp_block[BLOCK_SIZE];
50+
51+
cpp::byte *elem_i = get_internal(i);
52+
cpp::byte *elem_j = get_internal(j);
53+
54+
const size_t elem_size_rem = elem_size % BLOCK_SIZE;
55+
const cpp::byte *elem_i_block_end = elem_i + (elem_size - elem_size_rem);
56+
57+
while (elem_i != elem_i_block_end) {
58+
__builtin_memcpy(tmp_block, elem_i, BLOCK_SIZE);
59+
__builtin_memcpy(elem_i, elem_j, BLOCK_SIZE);
60+
__builtin_memcpy(elem_j, tmp_block, BLOCK_SIZE);
61+
62+
elem_i += BLOCK_SIZE;
63+
elem_j += BLOCK_SIZE;
64+
}
65+
66+
for (size_t n = 0; n < elem_size_rem; ++n) {
67+
cpp::byte tmp = elem_i[n];
68+
elem_i[n] = elem_j[n];
69+
elem_j[n] = tmp;
5570
}
5671
}
72+
73+
LIBC_INLINE size_t len() const { return array_len; }
74+
75+
// Make an Array starting at index |i| and length |s|.
76+
LIBC_INLINE ArrayGenericSize make_array(size_t i, size_t s) const {
77+
return ArrayGenericSize(get_internal(i), s, elem_size);
78+
}
79+
80+
// Reset this Array to point at a different interval of the same
81+
// items starting at index |i|.
82+
LIBC_INLINE void reset_bounds(size_t i, size_t s) {
83+
array_base = get_internal(i);
84+
array_len = s;
85+
}
5786
};
5887

59-
class Array {
60-
uint8_t *array;
61-
size_t array_size;
62-
size_t elem_size;
63-
Comparator compare;
88+
// Having a specialized Array type for sorting that knows at
89+
// compile-time what the size of the element is, allows for much more
90+
// efficient swapping and for cheaper offset calculations.
91+
template <size_t ELEM_SIZE> class ArrayFixedSize {
92+
cpp::byte *array_base;
93+
size_t array_len;
6494

65-
public:
66-
Array(uint8_t *a, size_t s, size_t e, Comparator c)
67-
: array(a), array_size(s), elem_size(e), compare(c) {}
68-
69-
uint8_t *get(size_t i) const { return array + i * elem_size; }
70-
71-
void swap(size_t i, size_t j) const {
72-
uint8_t *elem_i = get(i);
73-
uint8_t *elem_j = get(j);
74-
for (size_t b = 0; b < elem_size; ++b) {
75-
uint8_t temp = elem_i[b];
76-
elem_i[b] = elem_j[b];
77-
elem_j[b] = temp;
78-
}
95+
LIBC_INLINE cpp::byte *get_internal(size_t i) const {
96+
return array_base + (i * ELEM_SIZE);
7997
}
8098

81-
int elem_compare(size_t i, const uint8_t *other) const {
82-
// An element must compare equal to itself so we don't need to consult the
83-
// user provided comparator.
84-
if (get(i) == other)
85-
return 0;
86-
return compare.comp_vals(get(i), other);
99+
public:
100+
LIBC_INLINE ArrayFixedSize(void *a, size_t s)
101+
: array_base(reinterpret_cast<cpp::byte *>(a)), array_len(s) {}
102+
103+
// Beware this function is used a heuristic for cheap to swap types, so
104+
// instantiating `ArrayFixedSize` with `ELEM_SIZE > 100` is probably a bad
105+
// idea perf wise.
106+
static constexpr bool has_fixed_size() { return true; }
107+
108+
LIBC_INLINE void *get(size_t i) const { return get_internal(i); }
109+
110+
LIBC_INLINE void swap(size_t i, size_t j) const {
111+
alignas(32) cpp::byte tmp[ELEM_SIZE];
112+
113+
cpp::byte *elem_i = get_internal(i);
114+
cpp::byte *elem_j = get_internal(j);
115+
116+
__builtin_memcpy(tmp, elem_i, ELEM_SIZE);
117+
__builtin_memmove(elem_i, elem_j, ELEM_SIZE);
118+
__builtin_memcpy(elem_j, tmp, ELEM_SIZE);
87119
}
88120

89-
size_t size() const { return array_size; }
121+
LIBC_INLINE size_t len() const { return array_len; }
90122

91-
// Make an Array starting at index |i| and size |s|.
92-
LIBC_INLINE Array make_array(size_t i, size_t s) const {
93-
return Array(get(i), s, elem_size, compare);
123+
// Make an Array starting at index |i| and length |s|.
124+
LIBC_INLINE ArrayFixedSize<ELEM_SIZE> make_array(size_t i, size_t s) const {
125+
return ArrayFixedSize<ELEM_SIZE>(get_internal(i), s);
94126
}
95127

96-
// Reset this Array to point at a different interval of the same items.
97-
LIBC_INLINE void reset_bounds(uint8_t *a, size_t s) {
98-
array = a;
99-
array_size = s;
128+
// Reset this Array to point at a different interval of the same
129+
// items starting at index |i|.
130+
LIBC_INLINE void reset_bounds(size_t i, size_t s) {
131+
array_base = get_internal(i);
132+
array_len = s;
100133
}
101134
};
102135

103-
using SortingRoutine = void(const Array &);
104-
105136
} // namespace internal
106137
} // namespace LIBC_NAMESPACE_DECL
107138

0 commit comments

Comments
 (0)