More than once I claimed that using binary search doesn't improve performance of the insertion sort. For example, see answer here and comments here). Now I have time to substantiate my claim.
The only practical application of the insertion sort, where we actually do care about performance, is sorting almost sorted data; that is such data where each element is within a fixed distance from its final destination. Only this scenario is benchmarked.
First, the implementations of insertion sorts (insertion_sort.h
)
#include <algorithm>
template<typename It>
void straight_insertion_sort(It first, It last) {
for (auto cur = first + 1; cur < last; ++cur) {
auto val = *cur;
auto it = cur;
if (val < *first) {
for (it = cur; it > first; --it) {
*it = *(it - 1);
}
} else {
for (it = cur; val < *(it - 1); --it) {
*it = *(it - 1);
}
}
*it = val;
}
}
template<typename It>
void binary_insertion_sort(It first, It last) {
for (auto cur = first + 1; cur < last; ++cur) {
auto val = *cur;
auto insertion_point = std::lower_bound(first, cur - 1, *cur);
std:: copy_backward(insertion_point, cur - 1, cur);
*insertion_point = val;
}
}
The benchmarks shall run against an almost sorted collection. This is how the testcases are prepared. (incomplete_qsort.h
, the code is adapted from std::partition) example; cutoff is added to make the array almost sorted. After a call to incomplete_qsort
every element is at most cutoff
away from where it is supposed to be. NB: this is not really for a review, but only for completeness.
Note: I need c++14 here. c++11 does not allow auto
as an argument to lambda
.
#include <algorithm>
template<typename It>
void incomplete_qsort(It first, It last, size_t cutoff) {
if (std::distance(first, last) < cutoff) {
return;
}
auto pivot = *first;
auto mid1 = std::partition(first, last,
[pivot](const auto& em) {return em < pivot; });
auto mid2 = std::partition(mid1, last,
[pivot](const auto& em) {return !(pivot < em); });
incomplete_qsort(first, mid1, cutoff);
incomplete_qsort(mid2, last, cutoff);
}
This is the driver (benchmark.cpp
):
#include "incomplete_qsort.h"
#include "insertion_sort.h"
#include <chrono>
#include <iostream>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <random>
#include <vector>
using iter = std::vector<int>::iterator;
using sorter = void (*)(iter, iter);
double run_benchmark(std::vector<int>& data, sorter s) {
auto start = std::chrono::system_clock::now();
s(data.begin(), data.end());
auto end = std::chrono::system_clock::now();
std::chrono::duration<double> diff = end - start;
return diff.count();
}
int main(int argc, char ** argv)
{
std::random_device rd;
std::mt19937 g(rd());
for (int i = 12; i < 25; i++) {
auto size = 1 << i;
std::vector<int> data1(size);
std::vector<int> data2(size);
std::iota(data1.begin(), data1.end(), 0);
std::shuffle(data1.begin(), data1.end(), g);
incomplete_qsort(data1.begin(), data1.end(), 16);
std::copy(data1.begin(), data1.end(), data2.begin());
double duration1 = run_benchmark(data1, straight_insertion_sort);
double duration2 = run_benchmark(data2, binary_insertion_sort);
std::cout << std::setw(8) << size << ": "
<< std::setw(8) << duration1
<< std::setw(8) << duration2
<< " (" << duration2 / duration1 << ")"
<< '\n';
}
}
And finally the results, compiled with -O3
:
4096: 5.2e-05 0.000158 (3.03846)
8192: 9.1e-05 0.000269 (2.95604)
16384: 0.000161 0.000494 (3.06832)
32768: 0.000275 0.000968 (3.52)
65536: 0.000555 0.001823 (3.28468)
131072: 0.001171 0.003686 (3.14774)
262144: 0.002084 0.007765 (3.72601)
524288: 0.004457 0.015087 (3.38501)
1048576: 0.008304 0.030951 (3.72724)
2097152: 0.017204 0.063931 (3.71605)
4194304: 0.033697 0.132659 (3.93682)
8388608: 0.06833 0.277166 (4.05629)
16777216: 0.136164 0.569059 (4.17922)
binary / straight
. \$\endgroup\$