During the past few days, I have written a small C++14 library which mainly provides the cppsort::sort
function. It is designed to efficiently sort small arrays when their time is known at compile time, and to fall back to the standard library std::sort
otherwise.
While I am rather confident that the new sorting method achieves its goal, I still wrote a benchmarking program to compare cppsort::sort
and std::sort
. Since I am not used to writing benchmarks, I would like this benchmarking method reviewed in order to know whether there are bias which might affect the results. Any kind of review is welcome :)
The program generates shuffled arrays for every small array size (\$0\$ to \$64\$ in our case) and sorts them one million times per algorithm and returns series of integers to the standard output which correspond to the execution times of the algorithms in milliseconds (one line corresponds to the time of one algorithm, where the \$n\$th integer in the line corresponds to the time it took to sort one million of shuffled arrays of size \$n\$).
#include <algorithm>
#include <array>
#include <chrono>
#include <ctime>
#include <iostream>
#include <iterator>
#include <numeric>
#include <random>
#include <utility>
#include <cpp-sort/sort.h>
template<
typename RandomAccessIterable,
typename Compare = std::less<>
>
auto std_sort(RandomAccessIterable& iterable, Compare&& compare={})
-> void
{
std::sort(std::begin(iterable), std::end(iterable), std::forward<Compare>(compare));
}
template<
typename T,
std::size_t N,
typename SortFunction1,
typename SortFunction2
>
auto time_compare(SortFunction1 sort1, SortFunction2 sort2, std::size_t times)
-> std::array<std::chrono::milliseconds, 2u>
{
// Random numbers generator
thread_local std::mt19937_64 engine(std::time(nullptr));
// Generate shuffled array, the same for both algorithms
std::array<T, N> array;
std::iota(std::begin(array), std::end(array), 0);
std::shuffle(std::begin(array), std::end(array), engine);
// Time first algorithm
auto start = std::chrono::high_resolution_clock::now();
for (std::size_t i = 0 ; i < times ; ++i)
{
auto unsorted = array;
sort1(unsorted, std::less<>{});
}
auto end = std::chrono::high_resolution_clock::now();
auto duration1 = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
// Time second algorithm
start = std::chrono::high_resolution_clock::now();
for (std::size_t i = 0 ; i < times ; ++i)
{
auto unsorted = array;
sort2(unsorted, std::less<>{});
}
end = std::chrono::high_resolution_clock::now();
auto duration2 = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
return { duration1, duration2 };
}
template<typename T, std::size_t... Ind>
auto time_them(std::size_t size, std::index_sequence<Ind...>)
-> std::array<
std::array<
std::chrono::milliseconds,
sizeof...(Ind)
>,
2u
>
{
// Benchmark the sorts std::sort
std::array<std::array<std::chrono::milliseconds, 2u>, sizeof...(Ind)> results = {
time_compare<T, Ind>(
&cppsort::sort<T, Ind, std::less<>>,
&std_sort<std::array<T, Ind>, std::less<>>,
size
)...
};
// Results for cppsort::sort
std::array<std::chrono::milliseconds, sizeof...(Ind)> first = {
std::get<Ind>(results)[0u]...
};
// Results for std::sort
std::array<std::chrono::milliseconds, sizeof...(Ind)> second = {
std::get<Ind>(results)[1u]...
};
return { first, second };
}
int main()
{
using indices = std::make_index_sequence<64u>;
auto sorts_times = time_them<int>(1000000u, indices{});
for (auto&& sort_times: sorts_times)
{
for (auto&& time: sort_times)
{
std::cout << time.count() << ' ';
}
std::cout << '\n';
}
}
To use it, I call benchmark.exe > results.txt
then forward the results to a Python script which displays them on a graph with python plot.py results.txt
. Here is the said Python script:
import sys
import matplotlib.pyplot as plt
def fetch_results(fresults):
results = fresults.readline().split(' ')
results.pop()
return [int(elem) for elem in results]
if __name__ == '__main__':
# Results of timing functions
cppsort = []
stdsort = []
# Fetch the results
with open(sys.argv[1]) as f:
cppsort = fetch_results(f)
stdsort = fetch_results(f)
# Plot the results
xaxis = list(range(len(cppsort)))
line_cpp, = plt.plot(xaxis, cppsort)
line_std, = plt.plot(xaxis, stdsort)
plt.legend([line_cpp, line_std], ['cppsort::sort', 'std::sort'])
plt.xlabel('Number of elements to sort')
plt.ylabel('Execution time (ms)')
plt.show()
It generates the following kind of graph:
As we can see, it does it job to "prove" that cppsort::sort
is more performant than std::sort
for values of \$n\$ smaller than \$32\$ and it also makes it pretty obvious that the algorithm falls back to std::sort
for bigger values. That said I would like to know how I could stil improve the benchmarking method (is it possible to make the curve smoother?), and whether there is any bias in it.
Note: how the cppsort::sort
algorithm is implemented is off-topic for this question; you can find the explanation in the linked library's README. That said, you will at least need its interface to test the benchmarking method. For the sake of simplicity, I will provide a function with the exact same interface, but it will only forward the job to std::sort
instead. Trust me, you don't want 10k additional lines of code in the question.
namespace cppsort
{
template<
typename RandomAccessIterable,
typename Compare = std::less<>
>
auto sort(RandomAccessIterable& iterable, Compare&& compare={})
-> void
{
std::sort(
std::begin(iterable),
std::end(iterable),
std::forward<Compare>(compare)
);
}
template<
typename T,
std::size_t N,
typename Compare = std::less<>
>
auto sort(std::array<T, N>& array, Compare&& compare={})
-> void
{
std::sort(
std::begin(array),
std::end(array),
std::forward<Compare>(compare)
);
}
}
Again, for the sake of simplicity, I didn't provide the cppsort::sort
overload for fixed-size C arrays since it's not tested by the benchmarking method.