并行与顺序朴素快速排序
Parallel vs Sequential naïve quick sort
我正在尝试实现快速排序,它并行运行分区排序,如下所示:
// Returns patition points as pair
template<typename ExPo, std::random_access_iterator I, std::sentinel_for<I> S,
class Comp = std::ranges::less, class Proj = std::identity >
requires std::sortable<I, Comp, Proj>
std::pair<I, I> parition_points(ExPo policy, I first, S last, Comp comp = {}, Proj proj = {})
{
auto val = *std::next(first, std::distance(first, last) / 2);
auto mid1 = std::partition(policy, first, last, [&](const auto& cur) { return std::invoke(comp, std::invoke(proj, cur), std::invoke(proj, val)); }); // a < b
auto mid2 = std::partition(policy, mid1, last, [&](const auto& cur) { return !std::invoke(comp, std::invoke(proj, val), std::invoke(proj, cur)); }); // !(b < a)-> b >=a
return { mid1, mid2 };
}
// Quick sort
template<typename ExPo, std::random_access_iterator I, std::sentinel_for<I> S,
class Comp = std::ranges::less, class Proj = std::identity >
requires std::sortable<I, Comp, Proj>
void quicksort(ExPo policy, I first, S last, Comp comp = {}, Proj proj = {})
{
if (first == last) return;
const auto& [mid1, mid2] = parition_points(policy, first, last, std::ref(comp), std::ref(proj));
quicksort(policy, first, mid1, std::ref(comp), std::ref(proj));
quicksort(policy, mid2, last, std::ref(comp), std::ref(proj));
}
int main()
{
auto rv1 = getRandomVector(100000);
auto rv2 = rv1;
auto rv3 = rv1;
// std::sort parallel
auto sort1 = [&rv1]() { std::sort(std::execution::par, rv1.begin(), rv1.end());};
execute(sort1);
// quicksort in parallel
auto sort2 = [&rv2]() {quicksort(std::execution::par, rv2.begin(), rv2.end()); };
execute(sort2);
// quicksort in sequential
auto sort3 = [&rv3]() {quicksort(std::execution::seq, rv3.begin(), rv3.end()); };
execute(sort3);
return 0;
}
这里是 Godbolt 编译器资源管理器 link 我的例子。
我期望使用并行执行策略调用 std::partition 时性能更好,但结果或多或少相似,或者在某些情况下更差。
我做错了什么?
首先,由于并行性,您不能使用 godbolt 来演示加速(或减速)。上次我检查时,该服务为每个程序分配了一个硬件线程。
其次,默认情况下 gcc
不进行任何并行化。那里有两个并行后端,默认情况下使用的那个被称为“串行后端”。进行真正并行化的称为“TBB 后端”,您需要安装英特尔 TBB 库才能使用它。
最后但同样重要的是,“并行快速排序”的这种特殊实现一点也不好。它只尝试并行 运行 std::partition,如果有的话,这不会做太多。真正的并行快速排序 运行 对快速排序 的两次递归调用是并行的,当然它不会创建超过硬件支持的线程,因为它只是纯粹的开销零增益。
我能够快速而肮脏地调整您的代码,使并行版本实际上更快。这涉及到类似
static std::atomic<int> tc = 1;
if (!std::is_same_v<ExPo,std::execution::parallel_policy> || last-first<32 || tc >= 8)
{
const auto& [mid1, mid2] = parition_points(std::execution::seq, first, last, std::ref(comp), std::ref(proj));
quicksort(std::execution::seq, first, mid1, std::ref(comp), std::ref(proj));
quicksort(std::execution::seq, mid2, last, std::ref(comp), std::ref(proj));
}
else
{
++tc;
const auto& [mid1, mid2] = parition_points(policy, first, last, std::ref(comp), std::ref(proj));
auto fut = std::async(std::launch::async, [&](){quicksort(policy, first, mid1, std::ref(comp), std::ref(proj));});
quicksort(policy, mid2, last, std::ref(comp), std::ref(proj));
fut.wait();
--tc;
}
性能仍然很糟糕,但可以证明并行性带来的一些好处。
我正在尝试实现快速排序,它并行运行分区排序,如下所示:
// Returns patition points as pair
template<typename ExPo, std::random_access_iterator I, std::sentinel_for<I> S,
class Comp = std::ranges::less, class Proj = std::identity >
requires std::sortable<I, Comp, Proj>
std::pair<I, I> parition_points(ExPo policy, I first, S last, Comp comp = {}, Proj proj = {})
{
auto val = *std::next(first, std::distance(first, last) / 2);
auto mid1 = std::partition(policy, first, last, [&](const auto& cur) { return std::invoke(comp, std::invoke(proj, cur), std::invoke(proj, val)); }); // a < b
auto mid2 = std::partition(policy, mid1, last, [&](const auto& cur) { return !std::invoke(comp, std::invoke(proj, val), std::invoke(proj, cur)); }); // !(b < a)-> b >=a
return { mid1, mid2 };
}
// Quick sort
template<typename ExPo, std::random_access_iterator I, std::sentinel_for<I> S,
class Comp = std::ranges::less, class Proj = std::identity >
requires std::sortable<I, Comp, Proj>
void quicksort(ExPo policy, I first, S last, Comp comp = {}, Proj proj = {})
{
if (first == last) return;
const auto& [mid1, mid2] = parition_points(policy, first, last, std::ref(comp), std::ref(proj));
quicksort(policy, first, mid1, std::ref(comp), std::ref(proj));
quicksort(policy, mid2, last, std::ref(comp), std::ref(proj));
}
int main()
{
auto rv1 = getRandomVector(100000);
auto rv2 = rv1;
auto rv3 = rv1;
// std::sort parallel
auto sort1 = [&rv1]() { std::sort(std::execution::par, rv1.begin(), rv1.end());};
execute(sort1);
// quicksort in parallel
auto sort2 = [&rv2]() {quicksort(std::execution::par, rv2.begin(), rv2.end()); };
execute(sort2);
// quicksort in sequential
auto sort3 = [&rv3]() {quicksort(std::execution::seq, rv3.begin(), rv3.end()); };
execute(sort3);
return 0;
}
这里是 Godbolt 编译器资源管理器 link 我的例子。
我期望使用并行执行策略调用 std::partition 时性能更好,但结果或多或少相似,或者在某些情况下更差。
我做错了什么?
首先,由于并行性,您不能使用 godbolt 来演示加速(或减速)。上次我检查时,该服务为每个程序分配了一个硬件线程。
其次,默认情况下 gcc
不进行任何并行化。那里有两个并行后端,默认情况下使用的那个被称为“串行后端”。进行真正并行化的称为“TBB 后端”,您需要安装英特尔 TBB 库才能使用它。
最后但同样重要的是,“并行快速排序”的这种特殊实现一点也不好。它只尝试并行 运行 std::partition,如果有的话,这不会做太多。真正的并行快速排序 运行 对快速排序 的两次递归调用是并行的,当然它不会创建超过硬件支持的线程,因为它只是纯粹的开销零增益。
我能够快速而肮脏地调整您的代码,使并行版本实际上更快。这涉及到类似
static std::atomic<int> tc = 1;
if (!std::is_same_v<ExPo,std::execution::parallel_policy> || last-first<32 || tc >= 8)
{
const auto& [mid1, mid2] = parition_points(std::execution::seq, first, last, std::ref(comp), std::ref(proj));
quicksort(std::execution::seq, first, mid1, std::ref(comp), std::ref(proj));
quicksort(std::execution::seq, mid2, last, std::ref(comp), std::ref(proj));
}
else
{
++tc;
const auto& [mid1, mid2] = parition_points(policy, first, last, std::ref(comp), std::ref(proj));
auto fut = std::async(std::launch::async, [&](){quicksort(policy, first, mid1, std::ref(comp), std::ref(proj));});
quicksort(policy, mid2, last, std::ref(comp), std::ref(proj));
fut.wait();
--tc;
}
性能仍然很糟糕,但可以证明并行性带来的一些好处。