0

我在我的 C 程序中使用 Boehm-GC 进行垃圾收集。我正在尝试并行化一个适用于数组的 for 循环。数组是通过 GC_malloc 分配的。循环执行完毕后,程序中不再使用数组。我调用 GC_gcollect_and_unmap 来释放数组。但是,当我使用 openmp 并行化 for 循环时,循环执行完成后数组永远不会被释放。这是完全相同的程序,我只在循环周围添加#pragmas 以使其并行化。我尝试在有和没有 openmp 并行化的情况下并排查看汇编代码,我看到数组指针正在以类似的方式处理,并且没有看到额外的指针被保留在任何地方。唯一的区别是 for 循环是作为主函数中的一个简单循环实现的,但是当我并行化它时,openmp 创建一个新函数##name##._omp_fn 并调用它。无论如何,我需要做些什么来让 Boehm-GC 收集阵列吗?我很难发布 MWE,因为如果程序足够小,Boehm-GC 根本不会启动。

这是没有并行化的代码摘录。

  struct thing {
    float* arr;
    int size;
  }
  int l=10;
  static thing* get_randn(void) {
    thing* object = (thing*)GC_malloc(sizeof(struct {float* arr, int size}));
    object->arr=malloc(sizeof(float)*l);
    void finalizer(void *obj, void* client_data)
    { 
      printf("freeing %p\n", obj); 
      thing* object = (thing*)obj;
      free(object->arr);
    }
    GC_register_finalizer(object, &finalizer, NULL, NULL, NULL);
    float *arr = object->arr; 
    int t_id;
    for (t_id = 0; t_id<l; t_id++) { 
       torch_randn(arr+t_id); 
    } 
    return object;                          
  }                                 

上面的代码垃圾收集了函数产生的对象。以下是并行化的代码。

  struct thing {
    float* arr;
    int size;
  }
  int l=10;
  static thing* get_randn(void) {
    thing* object = (thing*)GC_malloc(sizeof(struct {float* arr, int size}));
    object->arr=malloc(sizeof(float)*l);
    void finalizer(void *obj, void* client_data)
    { 
      printf("freeing %p\n", obj); 
      thing* object = (thing*)obj;
      free(object->arr);
    }
    GC_register_finalizer(object, &finalizer, NULL, NULL, NULL);
    float *arr = object->arr; 
    int t_id;
    #pragma omp parallel num_threads(10)
    {
     #pragma omp for
     for (t_id = 0; t_id<l; t_id++) { 
       torch_randn(arr+t_id); 
     }
    } 
    return object;                          
  }                                 

对于此代码,对象不会被垃圾收集。仅通过 MWE 本身很难重现该问题,因为垃圾收集器不会启动小程序,但是当我使用完整的程序运行时,我正在观察这种行为。

4

1 回答 1

0

仅通过 MWE 本身很难重现该问题,因为垃圾收集器不会启动小程序,但是当我使用完整的程序运行时,我正在观察这种行为。

您可以通过调用强制垃圾回收GC_gcollect()

此外,Boehm-GC 肯定会释放并行部分中分配的内存/对象。但至少有一个警告:OpenMP 在内部使用线程池。这意味着线程不一定在并行部分结束后终止。那些池化和空闲线程可能仍然具有对堆上对象的引用。

考虑以下程序,它并行运行四个线程并为每个线程分配一千个“对象”:

#define GC_THREADS
#include <assert.h>
#include <stdio.h>
#include <omp.h>
#include <gc.h>

#define N_THREADS 4
#define N 1000

// count of finalized objects per thread
static int counters[N_THREADS];

void finalizer(void *obj, void* client_data)
{
#pragma omp atomic
    counters[*(int*)obj]++;
}

int main(void)
{
    GC_INIT();
    GC_allow_register_threads();

    int i;
    for(i = 0; i < N_THREADS; i++) {
        counters[i] = 0;
    }

    // allocate lots integers and store the thread id in it
    // execute N iterations per thread
#pragma omp parallel for num_threads(4) schedule(static, N)
    for (i = 0; i < N_THREADS*N; i++)
    {
        struct GC_stack_base sb;
        GC_get_stack_base(&sb);
        GC_register_my_thread(&sb);

        int *p;
        p = (int*)GC_MALLOC(4);
        GC_REGISTER_FINALIZER(p, &finalizer, NULL, NULL, NULL);
        *p = omp_get_thread_num();
    }

    GC_gcollect();
    for(i = 0; i < N_THREADS; i++) {
        printf("finalized objects in thread %d: %d of %d\n", i, counters[i], N);
    }
    return 0;
}

示例输出:

finalized objects in thread 0: 1000 of 1000
finalized objects in thread 1: 999 of 1000
finalized objects in thread 2: 999 of 1000
finalized objects in thread 3: 999 of 1000

这些数字意味着线程 1 到 3 是池化的,并且仍然持有对最后一次迭代对象的引用。线程 0 是继续执行的主线程,因此失去了堆栈上最后一次迭代的引用。

编辑: @maddy:我认为这与寄存器或编译器优化没有任何关系。根据经验,编译器只能执行保证不会改变程序行为的优化。诚然,您的问题可能是极端情况。

根据Wikipedia,Boehm-GC 在程序堆栈中查找引用。根据编译器如何将 openmp pragma 转换为代码,很可能包含对堆的引用的堆栈帧在线程进入空闲状态时仍然有效。在这种情况下,Boehm-GC 根据定义无法最终确定引用的对象/内存。但是这个恕我直言很难推理。您需要很好地了解您的编译器对 openmp pragma 的作用以及 Boehm-GC 如何准确地分析程序堆栈。

关键是:一旦您重用线程(通过使用 openmp 运行其他东西),池线程的堆栈将被覆盖,并且 Boehm-GC 将能够从先前的并行迭代中回收内存。从长远来看,您不会泄漏内存。

于 2019-05-17T14:08:26.823 回答