Почему вещи, которые используются внутри параллельных блоков openmp, впоследствии не собираются Boehm GC?

Я использую Boehm-GC в своей программе на C для сборки мусора. Я пытаюсь распараллелить цикл for, который работает с массивом. Массив выделяется через GC_malloc. Когда цикл завершается, массив больше не используется в программе. Я вызываю GC_gcollect_and_unmap, который освобождает массив. Однако, когда я распараллеливаю цикл for с помощью openmp, массив никогда не освобождается после выполнения цикла. Это точно такая же программа, я только добавляю #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, потому что сборщик мусора не срабатывает для небольших программ, но я наблюдаю такое поведение, когда запускаю свою полную программу.


person maddy99    schedule 10.05.2019    source источник
comment
Как насчет минимально воспроизводимого примера, демонстрирующего проблему? Прозаическое описание, сделанное Истелфом, редко охватывает все важные детали.   -  person John Bollinger    schedule 10.05.2019


Ответы (1)


Трудно воспроизвести проблему только через 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: я не думаю, что это имеет какое-либо отношение к регистрам или оптимизации компилятора. Как правило, компилятор может выполнять только те оптимизации, которые гарантированно не изменят поведение программы. По общему признанию, ваша проблема может быть угловым случаем.

Согласно Википедии, Boehm-GC ищет ссылки в стеке программы. В зависимости от того, как компилятор преобразует прагмы openmp в код, вполне может случиться так, что кадр стека, содержащий ссылку на кучу, все еще будет действительным, когда поток переходит в состояние ожидания. В этом случае Boehm-GC по определению не может финализировать указанный объект/память. Но трудно рассуждать об этом ИМХО. Вам нужно хорошо понимать, что ваш компилятор делает с прагмами openmp и как именно Boehm-GC анализирует программный стек.

Дело в том, что как только вы повторно используете потоки (запустив что-то еще с помощью openmp), стеки потоков в пуле будут перезаписаны, и Boehm-GC сможет восстановить память из предыдущей параллельной итерации. В долгосрочной перспективе утечки памяти не будет.

person Gregor Budweiser    schedule 17.05.2019
comment
Как вы думаете, это потому, что потоки не обязательно завершаются, или потому что эти потоки оставляют указатели на переменные в регистрах, и если программа небольшая и нет большого давления на регистры, gcc не будет перезаписывать эти регистры, если это не так. они не нужны (потому что gcc так оптимизирован). Но сборщик мусора может только сканировать регистры и кучу, и если он видит указатель, он предполагает, что переменная жива, компилятор не сообщает ему, что переменная мертва, а регистр просто содержит старое значение, которое не быть перезаписанным - person maddy99; 18.05.2019