为啥并行读取数组会导致内存泄漏?

Posted

技术标签:

【中文标题】为啥并行读取数组会导致内存泄漏?【英文标题】:why does reading an array in parallel cause memory leaks?为什么并行读取数组会导致内存泄漏? 【发布时间】:2019-09-30 02:22:09 【问题描述】:

我正在使用 openmp 并行读取一个数组。下面是一个最小的可重现示例:

#include <cstdint>
#include <cstdlib>
#include <immintrin.h>
#include <iostream>
#include <memory>
#include <omp.h>

int main(int argc, char* argv[])
  // align to cache line, which is 512 bits or 64 bytes
  size_t actualSize = 2048;

  uint8_t* array = static_cast<uint8_t *>(aligned_alloc(64, actualSize));
  for(size_t i = 0; i < actualSize; i++)
    // initialize values
    array[i] = rand() % 256;
  

  __m256i sum_v = _mm256_setzero_si256 ();
  #pragma omp parallel for
  for (size_t i = 0; i < actualSize; i+=32)
    __m256i v1 = _mm256_load_si256((const __m256i *) array+i);
    // i understand that there is a race condition here, but I'm just
    // concerned with the memory leaks
    sum_v = _mm256_add_epi8 (sum_v, v1);
  

  // just to keep compiler from optimizing out sum_v
  uint8_t result = _mm256_extract_epi8 (sum_v, 0);

  std::cout << "result: " << result << std::endl;

  free(array);
  return 0;

这是在我的计算机上测量内存带宽的尝试,我最终会为不同的实际大小计时。

我用g++ -Wall -g -std=c++1y -march=native -mtune=native -fopenmp -O3 -g minimal-memleaks.cpp 编译这个。当我使用valgrind ./a.out 运行这个程序时,我得到一个内存泄漏,其中一部分复制到下面

==7688== Thread 8:
==7688== Invalid read of size 32
==7688==    at 0x108D30: _mm256_add_epi8 (avx2intrin.h:107)
==7688==    by 0x108D30: main._omp_fn.0 (minimal-memleaks.cpp:25)
==7688==    by 0x51DB95D: ??? (in /usr/lib/x86_64-linux-gnu/libgomp.so.1.0.0)
==7688==    by 0x5FA66DA: start_thread (pthread_create.c:463)
==7688==    by 0x551588E: clone (clone.S:95)
==7688==  Address 0x61e1980 is 29,280 bytes inside an unallocated block of size 4,077,760 in arena "client"

此处提供完整输出:https://pastebin.com/qr0W9FGD

我似乎不明白为什么。起初我认为循环超过了我分配的 2048 字节,但我的数学表明它不应该。我读取了 32 个块,当循环应该停止时,将 32 添加到 i 最终将等于 2048。我还认为主线程可能在子线程之前关闭,但我的研究表明,在#pragma omp parallel for 循环创建的线程关闭之前,主线程不会关闭。这是不正确的吗?

感谢您提供的任何帮助。

【问题讨论】:

内存泄漏是(大致)分配了额外的内存,您不再需要并且应该取消分配。此错误表示您正在阅读不应该阅读的内存。 指针算法:(const __m256i *) array+i 不是您期望的位置 简而言之,您在array+i 周围缺少一组括号,在添加之前评估演员表。否则你的想法很好。 @Qubit 您对操作顺序的评论是我所缺少的。谢谢! 【参考方案1】:

这不是内存泄漏。您正在读取和/或损坏内存。

 for (size_t i = 0; i < actualSize; i+=32)
   __m256i v1 = _mm256_load_si256((const __m256i *) array+i);

你跑到数组的末端了,这里。 actualSize 是您分配的数组的大小以字节为单位

__m256i 是一种 32 字节长的数据类型。

(const __m256i *) array

这会将指针转换为指向 32 字节对象的指针。

指针加法在 C++ 中的工作方式是,向指针添加 1 会使指针前进到下一个对象,所以

 array+1

next 32 字节对象所在的位置。 array 之后的 32 个字节。

因此,如果您计算出您的 for 循环最终读取了哪个地址,那么很明显它正在从您的数组末端跑到永远不会到达的地方,因此 valgrind 对您咆哮。

您的for 循环应该是:

for (size_t i = 0; i < actualSize/32; ++i)
  __m256i v1 = _mm256_load_si256((const __m256i *) array+i);

【讨论】:

执行以下操作不是也有效吗? for (size_t i = 0; i &lt; actualSize; i+=32) __m256i v1 = _mm256_load_si256((const __m256i *) (array+i)); 这样,数组在被强制转换之前增加了i 字节? 是的,编译器很可能生成相同的代码。

以上是关于为啥并行读取数组会导致内存泄漏?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 Netty ByteBuf.readBytes 会导致内存泄漏?

为啥使用“新”会导致内存泄漏?

为啥这会导致内存泄漏?

为啥这个 INotifyCollectionChanged 会导致内存泄漏?

为啥“[[UIDevice currentDevice] identifierForVendor]”会导致内存泄漏?

为啥使用 Activity 上下文会导致 Context 内存泄漏