【问题标题】:C++ heap memory performance improvementC++ 堆内存性能提升
【发布时间】:2011-04-16 01:57:33
【问题描述】:

我正在编写一个需要大量堆内存的函数。是否可以告诉编译器这些数据将在特定的for 循环中被频繁访问,从而提高性能(通过编译选项或类似方法)?

我不能使用堆栈的原因是我需要存储的元素数量很大,如果我尝试这样做会出现分段错误。

现在代码正在运行,但我认为它可以更快。

更新: 我正在做这样的事情

vector< set<uint> > vec(node_vec.size());
for(uint i = 0; i < node_vec.size(); i++)
  for(uint j = i+1; j < node_vec.size(); j++)
    // some computation, basic math, store the result in variable x
      if( x > threshold ) {
         vec[i].insert(j);
         vec[j].insert(i);
      }

一些细节:
- 我使用了 hash_set,几乎没有改进,除了 hash_set 并非在我拥有的所有机器中都可用以用于模拟目的
- 我尝试使用数组在堆栈上分配 vec,但正如我所说,如果元素数量太大,我可能会遇到分段错误

如果 node_vec.size() 等于 k,其中 k 大约为几千,我希望 vec 比 node_vec 大 4 或 5 倍。考虑到我必须多次运行它,使用这个数量级的代码似乎很慢。当然,我使用多线程来并行化这些调用,但我不能让函数本身运行得比我现在看到的快得多。

是否有可能,例如,在高速缓存中分配 vec 以进行快速数据检索,或类似的东西?

【问题讨论】:

  • 您认为编译器可以优化什么?内存就是内存,堆和栈在硬件上没有区别。访问速度取决于使用模式和缓存算法,但编译器对此无能为力。
  • 为什么不对模板使用分配器?

标签: c++ memory-management heap-memory stack-memory


【解决方案1】:

我正在编写一个需要大量堆内存的函数......将在特定的 for 循环中频繁访问

这不是您可以在编译器级别真正优化的东西。我认为您担心的是您有很多内存可能是“陈旧的”(已分页),但在特定时间点您需要遍历所有内存,可能需要多次迭代并且您不想要内存要调出到磁盘的页面。

您需要研究特定于平台的策略以提高性能。使用mlockallVirtualLock 可以将页面保存在内存中,但您确实不需要这样做。但是,请确保您知道将应用程序的内存页面锁定到 RAM 中的含义。您正在占用其他进程的内存。

您可能还想调查low fragmentation heap(但它可能与此问题完全无关)和this page,它描述了与for 循环相关的缓存行。

后一页是关于 CPU 在内存访问方面的工作原理(您通常不必关心的细节)。

示例 1:内存访问和性能 与循环 1 相比,您预计循环 2 的运行速度会快多少?

int[] arr = new int[64 * 1024 * 1024];

// Loop 1
for (int i = 0; i < arr.Length; i++) arr[i] *= 3;

// Loop 2
for (int i = 0; i < arr.Length; i += 16) arr[i] *= 3;

第一个循环将数组中的每个值乘以 3,第二个循环仅每 16 次乘一次。 第二个循环只完成了第一个循环的 6% 的工作,但在现代机器上,两个 for 循环的时间差不多:80 和 78 毫秒分别在我的机器上。

【讨论】:

    【解决方案2】:

    更新

    vector< set<uint> > vec(node_vec.size());
    for(uint i = 0; i < node_vec.size(); i++)
      for(uint j = i+1; j < node_vec.size(); j++)
        // some computation, basic math, store the result in variable x
          if( x > threshold ) {
             vec[i].insert(j);
             vec[j].insert(i);
          }
    

    这仍然没有显示太多,因为我们不知道条件x &gt; threshold 多久会为真。如果x &gt; threshold 经常为真,那么std::set 可能是瓶颈,因为它必须为您插入的每个uint 进行动态内存分配。

    我们也不知道“一些计算”实际上是什么意思/做了/是什么。如果它做得很多,或者以错误的方式做,这可能是瓶颈。

    而且我们不知道您需要如何访问结果。

    总之,凭直觉:

        vector<pair<int, int> > vec1;
        vector<pair<int, int> > vec2;
    
        for (uint i = 0; i < node_vec.size(); i++)
        {
            for (uint j = i+1; j < node_vec.size(); j++)
            {
                // some computation, basic math, store the result in variable x
                if (x > threshold)
                {
                    vec1.push_back(make_pair(i, j));
                    vec2.push_back(make_pair(j, i));
                }
            }
        }
    

    如果您可以使用该表单中的结果,那么您就完成了。否则你可以做一些后期处理。只是不要再次将其复制到std::set 中(显然)。尽量坚持std::vector&lt;POD&gt;。例如。你可以像这样在向量中建立一个索引:

        // ...
        vector<int> index1 = build_index(node_vec.size(), vec1);
        vector<int> index2 = build_index(node_vec.size(), vec2);
        // ...
    }    
    
    vector<int> build_index(size_t count, vector<pair<int, int> > const& vec)
    {
        vector<int> index(count, -1);
    
        size_t i = vec.size();
        do
        {
            i--;
            assert(vec[i].first >= 0);
            assert(vec[i].first < count);
            index[vec[i].first] = i;
        }
        while (i != 0);
    
        return index;
    }
    

    ps.:我几乎可以肯定你的循环不是内存限制的。但不能确定......如果你没有向我们展示的“节点”真的很大,它可能仍然是。


    原答案:

    没有简单的I_will_access_this_frequently_so_make_it_fast(void* ptr, size_t len)-kind-of 解决方案。

    你可以做一些事情。

    1. 确保编译器可以“看到”在关键循环中调用的每个函数的实现。编译器能够“看到”实现所必需的取决于编译器。不过有一种方法可以确定:在循环之前在同一个翻译单元中定义所有相关函数,并将它们声明为inline

      这也意味着您应该在这些关键循环中以任何方式调用“外部”函数。 “外部”函数是指系统调用、运行时库的东西或在 DLL/SO 中实现的东西。也不要调用虚函数,不要使用函数指针。而且或当然不分配或释放内存(在关键循环内)。

    2. 确保使用最佳算法。如果算法的复杂度高于必要,则线性优化没有实际意义。

    3. 使用尽可能小的类型。例如。如果signed char 可以完成这项工作,请不要使用int。这是我通常不会推荐的东西,但是在处理大量内存时,它可以大大提高性能。尤其是在非常紧凑的循环中。

    4. 如果您只是复制或填充内存,请使用memcpymemset禁用如果块大于大约 50 到 100 个字节,则这两个函数的内在版本。

    5. 确保以缓存友好的方式访问数据。最佳选择是“流式传输” - 即使用升序或降序地址访问内存。您可以一次“跳”一些字节,但不要跳得太远。最糟糕的是随机访问一大块内存。例如。如果您必须处理二维矩阵(如位图图像),其中 p[0] 到 p[1] 是“向右”(x + 1)的一步,请确保内部循环递增 x 和外部循环递增 y。如果您以相反的方式执行此操作,则性能会要差很多

    6. 如果您的指针是无别名的,您可以告诉编译器(如何完成取决于编译器)。如果您不知道无别名意味着什么,我建议您搜索网络和编译器的文档,因为解释超出了范围。

    7. 如果合适,使用内部 SIMD 指令。

    8. 如果您知道近期将需要哪些内存位置,请使用显式预取指令。

    【讨论】:

      【解决方案3】:

      您不能使用编译器选项来做到这一点。根据您的使用情况(插入、随机访问、删除、排序等),您可能会得到更合适的容器。

      【讨论】:

        【解决方案4】:

        编译器已经可以看到数据在循环内被频繁访问。

        【讨论】:

        • 如果内存位置被传递到第三方库,并且第三方库回调到主代码,该内存位置说“在这里,用这个做点什么!”编译器不知道调用回调的潜在内存位置。
        • @taspeotis:当然不是。那么手动告诉它有什么意义呢?它对 3rd 方代码无能为力。
        【解决方案5】:

        假设您在执行循环之前只从堆中分配一次数据,请注意,正如@lvella 一样,内存就是内存,如果频繁访问它应该在执行期间有效地缓存。

        【讨论】:

          猜你喜欢
          • 2017-10-08
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多