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

时间:2016-07-31 19:57:16

标签: c++ performance stl performance-testing

1。问题描述

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

  1. 从外部程序中获取新的向量(在同一运行期间,所有向量具有相同的维度)。
  2. 检查(ASAP)此容器中是否已有新点:
    • 如果存在 - 跳过许多昂贵的步骤并执行其他步骤;
    • 如果没有 - 插入到容器中(在容器中排序并不重要)并执行其他步骤。
  3. 事先,我不知道我将拥有多少向量,但是最大数量是预先规定的并且等于= 100000.而且,每次迭代我总是只获得一个新向量。因此,在开始时,这些新载体中的大多数是独特的并且将被插入容器中,但是后来很难提前预测。很多将取决于唯一向量和容差值的定义。

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

    2。选择合适的容器

    我做了一些评论,并从我在S. Meyers - Effective STL中找到的内容第1项:小心选择容器

      

    查找速度是否至关重要?如果是这样,你会想看   在散列容器(见第25项),分类向量(见第23项),和   标准关联容器 - 可能按此顺序。

    以及我在惊人的David Moore的流程图Choosing a Container中所看到的,看起来来自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())
    4. 3。对不同容器进行基准测试

      在所有实验中,我使用随机生成的三维向量建模情境,其中填充了区间[0,1)的实际值。

      修改

      二手编译器:Apple LLVM版本7.0.2(clang-700.1.81)

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

      3.1使用未排序的vector

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

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

      使用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() << " s\n";
      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() << " s\n";
      std::cout << "set size = " << sda.size() << std::endl;
      

      其中排序标准基于有缺陷(打破严格的弱排序)answer。目前我想(大致)看一下我对不同容器的期望,然后决定哪一个是最好的。

      // 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 | 100000 |

      | 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, The C++ Standard Library: A Tutorial and Reference之后的期望

        

      只要你只插入,删除和查找具体的元素   值,无序容器提供最佳的运行时行为   因为所有这些操作已经摊销了不变的复杂性。

      非常高但谨慎,

        

      提供良好的哈希函数比听起来更棘手。

      使用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() << " s\n";
        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/100000 |

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

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

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

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

      编辑:这是最意想不到的行为。为无序集开启-O3优化,性能下降得非常糟糕。更令人惊讶的是,独特元素的数量取决于优化标志,这不应该仅仅意味着我必须提供更好的散列函数!?

      4。打开问题

      1. 正如我所知,在使用std::vector::reserve(100000)时,可能的唯一向量的最大大小是否合理?
      2. 根据The C++ Programming Language by Bjarne Stroustrup reserve不会对效果产生重大影响:

          

        在我阅读时,我曾经小心使用reserve()   向量。我很惊讶地发现,基本上我的所有用途,   调用reserve()并没有显着影响性能。默认   增长战略与我的估计一样有效,所以我停了下来   尝试使用reserve()来提高性能。

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

        1. 如何为

        2. 中描述的情况提供更好的哈希函数
        3. 关于此比较公平性的一般评论。我该如何改进呢?

        4. 如何使我的代码更好,更高效的任何一般性评论都非常受欢迎 - 我很乐意向你学习,伙计们! ;)

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

          P.P.S。请随意纠正我可怜的英语。

1 个答案:

答案 0 :(得分:0)

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

为了进行比较,您可以将ArrayEqual与EuclideanDistance2结合使用:

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
};