用于快速插入和查找n维实向量的适当容器(提供初始基准)

Appropriate container for the fast insertion and lookup of n-dimensional real vectors (initial benchmarking provided)

本文关键字:基准 插入 查找 向量 用于      更新时间:2023-10-16

1。问题描述

我正在尝试选择最合适(高效)的容器来存储由浮点数组成的唯一n维向量。解决整个问题,最重要的步骤(与问题相关)包括:

  1. 从外部程序获取一个新向量(在同一次运行中,所有向量都具有相同的维度)
  2. 检查(尽快)此容器中是否已有新点:
    • 如果存在-跳过许多昂贵的步骤并执行其他步骤
    • 如果没有-插入容器中(在容器中排序并不重要),然后执行其他步骤

事先,我不知道我会有多少个向量,但最大数量是预先规定的,等于100000。此外,每次迭代我总是只得到一个新的向量。因此,在一开始,这些新矢量中的大多数都是唯一的,并将被插入容器中,但后来很难提前预测。这在很大程度上取决于唯一矢量和公差值的定义。

因此,我的目标是为这种(类型)情况选择合适的容器。

2.选择合适的容器

我做了一些回顾,并根据我在S.Meyers-有效STL中的发现项目1:小心选择容器

查找速度是一个关键考虑因素吗?如果是,你会想看看散列容器(见第25项)、排序向量(见第23项),以及标准的关联容器——可能是按照这个顺序。

以及我在David Moore令人惊叹的流程图《选择容器》中看到的情况来看,s.Meyers的所有三个建议选项,第1项都值得更广泛的调查。

2.1复杂性(基于cppreference.com)

让我们从非常简短的研究开始,看看所有三个单独考虑的选项的查找和插入过程的理论复杂性:

  1. 对于矢量
    • find_if()-线性O(n)
    • push_back()-常数,但如果新的size()大于旧的capacity(),则会导致重新分配
  2. 对于集合
    • insert()-容器大小的对数,O(log(size()))
  3. 对于无序集
    • insert()-平均情况:O(1),最坏情况O(size())

3.对不同容器进行基准测试

在所有实验中,我用随机生成的三维向量对情况进行了建模,这些向量填充了区间[0,1)的实数

编辑:

使用过的编译器:Apple LLVM 7.0.2版(clang-700.1.81)

在发布模式下编译,具有优化级别-O3,没有任何优化级别。

3.1使用未排序的vector

首先,为什么矢量未排序?我的场景与S.Meyers-Effective STLItem 23中描述的场景有很大不同:考虑用排序向量替换关联容器因此,我认为在这种情况下使用排序向量没有任何好处。

其次,假设两个向量xy相等,如果EuclideanDistance2(x,y) < tollerance^2。考虑到这一点,我最初(可能相当糟糕)使用vector的实现如下:

使用vector容器实现的基准测试部分:

// create a vector of double arrays (vda)
std::vector<std::array<double, N>> vda;
const double tol = 1e-6;  // set default tolerance
// record start time
auto start = std::chrono::steady_clock::now();
// Generate and insert one hundred thousands new double arrays
for (size_t i = 0; i < 100000; ++i) {
// Get a new random double array (da)
std::array<double, N> da = getRandomArray();
auto pos = std::find_if(vda.begin(), vda.end(),  // range
[=, &da](const std::array<double, N> &darr) {  // search criterion
return EuclideanDistance2(darr.begin(), darr.end(), da.begin()) < tol*tol;
});
if (pos == vda.end()) {
vda.push_back(da);  // Insert array
}
}
// record finish time
auto end = std::chrono::steady_clock::now();
std::chrono::duration<double> diff = end - start;
std::cout << "Time to generate and insert unique elements into vector: "
<< diff.count() << " sn";
std::cout << "vector's size = " << vda.size() << std::endl;

这里随机n-维向量(n维数组)生成:

// return an array of N uniformly distributed random numbers from 0 to 1
std::array<double, N> getRandomArray() {
// Engines and distributions retain state, thus defined as static
static std::default_random_engine e;                    // engine
static std::uniform_real_distribution<double> d(0, 1);  // distribution
std::array<double, N> ret;
for (size_t i = 0; i < N; ++i) {
ret[i] = d(e);
}
return ret;
}

计算欧几里得距离的平方:

// Return Squared Euclidean Distance
template <typename InputIt1, typename InputIt2>
double EuclideanDistance2(InputIt1 beg1, InputIt1 end1, InputIt2 beg2) {
double val = 0.0;
while (beg1 != end1) {
double dist = (*beg1++) - (*beg2++);
val += dist*dist;
}
return val;
}

3.1.1测试矢量的性能

在下面混乱的表格中,我总结了10次独立运行的平均执行时间,以及取决于不同公差(eps)值的最终容器的大小。公差值越小,唯一元素的数量越高(插入次数越多),而公差值越高,唯一向量的数量越少,查找时间越长。

|eps |有-O3标志/没有优化标志的时间|大小|

|1e-6 | 13.1496/111.83 | 100000 |

|1e-3 | 14.1295/114.254 | 99978 |

|1e-2|10.5931/90.674 |82868|

|1e-1|0.0551718/0.462546|749|

从结果来看,使用向量最耗时的部分似乎是查找(find_if())。

编辑:此外,很明显,-O3优化在提高矢量性能方面做得很好。

3.2使用set

使用set容器实现的基准部分:

// create a set of double arrays (sda) with a special sorting criterion
std::set<std::array<double, N>, compare_arrays> sda;
// create a vector of double arrays (vda)
std::vector<std::array<double, N>> vda;
// record start time
auto start = std::chrono::steady_clock::now();
// Generate and insert one hundred thousands new double arrays
for (size_t i = 0; i < 100000; ++i) {
// Get a new random double array (da)
std::array<double, N> da = getRandomArray();
// Inserts into the container, if the container doesn't already contain it.
sda.insert(da);
}
// record finish time
auto end = std::chrono::steady_clock::now();
std::chrono::duration<double> diff = end - start;
std::cout << "Time to generate and insert unique elements into SET: "
<< diff.count() << " sn";
std::cout << "set size = " << sda.size() << std::endl;

其中排序标准基于有缺陷的(打破严格的弱排序)答案。目前,我想(大致)看看我能从不同的容器中期待什么,然后再决定哪一个是最好的。

// return whether the elements in the arr1 are “lexicographically less than”
// the elements in the arr2
struct compare_arrays {
bool operator() (const std::array<double, N>& arr1,
const std::array<double, N>& arr2) const {
// Lexicographical comparison compares using element-by-element rule
return std::lexicographical_compare(arr1.begin(), arr1.end(),  // 1st range
arr2.begin(), arr2.end(),  // 2nd range
compare_doubles);   // sorting criteria
}
// return true if x < y and not within tolerance distance
static bool compare_doubles(double x, double y) {
return (x < y) && !(fabs(x-y) < tolerance);
}
private:
static constexpr double tolerance = 1e-6;  // Comparison tolerance
};

3.2.1试验装置的性能

在下表中,我根据不同的公差(eps)值总结了执行时间和容器大小。使用了相同的eps值,但对于集合,等效定义不同。

|eps |有-O3标志/没有优化标志的时间|大小|

|1e-6|0.041414/1.51723|10000|

|1e-3|0.0457692/0.136944|99988|

|1e-2|0.0501/0.13808|90828|

|1e-1 | 0.0149597/0.0777621 | 2007 |

与矢量方法相比,性能差异很大。现在主要关注的是有缺陷的排序标准。

编辑:-O3优化也很好地提高了机组的性能。

3.3使用未排序的集合

最后,正如我在读了一点Josuttis的《C++标准库:教程和参考》后所期望的那样,我渴望尝试无序集

只要您只插入、擦除和查找具有特定值,无序容器提供最佳运行时行为因为所有这些操作都分摊了恒定的复杂性。

确实很高,但很谨慎,因为

提供一个好的哈希函数比听起来更难。

使用unordered_set容器实现的基准测试部分:

// create a unordered set of double arrays (usda)
std::unordered_set<std::array<double, N>, ArrayHash, ArrayEqual> usda;
// record start time
auto start = std::chrono::steady_clock::now();
// Generate and insert one hundred thousands new double arrays
for (size_t i = 0; i < 100000; ++i) {
// Get a new random double array (da)
std::array<double, N> da = getRandomArray();
usda.insert(da);
}
// record finish time
auto end = std::chrono::steady_clock::now();
std::chrono::duration<double> diff = end - start;
std::cout << "Time to generate and insert unique elements into UNORD. SET: "
<< diff.count() << " sn";
std::cout << "unord. set size() = " << usda.size() << std::endl;

其中一个天真的散列函数:

// Hash Function
struct ArrayHash {
std::size_t operator() (const std::array<double, N>& arr) const {
std::size_t ret;
for (const double elem : arr) {
ret += std::hash<double>()(elem);
}
return ret;
}
};

和等价标准:

// Equivalence Criterion
struct ArrayEqual {
bool operator() (const std::array<double, N>& arr1,
const std::array<double, N>& arr2) const {
return EuclideanDistance2(arr1.begin(), arr1.end(), arr2.begin()) < tol*tol;
}
private:
static constexpr double tol = 1e-6;  // Comparison tolerance
};

3.3.1测试未分拣机组的性能

在下面最后一个混乱的表格中,我再次总结了执行时间和容器的大小,这取决于不同的容差(eps)值。

|eps |有-O3标志/没有优化标志的时间|大小|

|1e-6 |57.4823/0.0590703 |100000/10000|

|1e-3 |57.9588/0.0618149 |99978/10000|

|1e-2|43.2816/0.0595529|82873/10000|

|1e-1|0.238788/0.057897|781/99759|

很快,与其他两种方法相比,执行时间是最好的,然而,即使使用非常宽松的容差(1e-1),几乎所有的随机向量都被确定为唯一的。所以,在我的情况下,节省了查找的时间,但却浪费了更多的时间来处理我问题的其他昂贵步骤。我想,这是因为我的哈希函数真的很天真吗?

编辑:这是最意想不到的行为。对无序集开启-O3优化,性能大幅度下降。更令人惊讶的是,唯一元素的数量取决于优化标志,而优化标志不应该是。这可能只是意味着,我必须提供一个更好的哈希函数!?

4.开放式问题

  1. 正如我之前所知,使用std::vector::reserve(100000)有意义吗

根据Bjarne Stroustrup的C++编程语言,reserve对性能没有太大影响:

当我阅读矢量。我惊讶地发现,对于我的所有用途,调用reserve()并没有显著影响性能。默认值增长策略和我的估计一样有效,所以我停止了尝试使用reserve()来提高性能。

我使用向量和eps=1e-6与reserve() = 100000重复了相同的实验,在这种情况下,总执行时间为111.461(s),而没有reserve()时为111.83(s)。因此,差异可以忽略不计。

  1. 如何为中描述的情况提供更好的哈希函数

  2. 关于这种比较的公正性的一般性意见。我该如何改进?

  3. 任何关于如何让我的代码变得更好、更高效的一般性评论都是非常受欢迎的——我喜欢向你们学习,伙计们!)

p.S.最后,(在StackOverflow中)是否有适当的markdown支持来创建表?在这个问题(基准测试)的最后版本中,我想放一张最后的总结表。

附言:请随意纠正我糟糕的英语。

对于hash函数,最好使用^=而不是+=使hash更随机。

为了进行比较,您可以将ArrayEqual与欧几里得距离2:相结合

struct ArrayEqual {
bool operator() (const std::array<double, N>& arr1,
const std::array<double, N>& arr2) const {
auto beg1 = arr1.begin(), end1 = arr1.end(),  beg2 = arr2.begin();
double val = 0.0;
while (beg1 != end1) {
double dist = (*beg1++) - (*beg2++);
val += dist*dist;
if (val >= tol*tol)
return false;
}
return true;
}
private:
static constexpr double tol = 1e-6;  // Comparison tolerance
};