Python: поведение сборщика мусора с помощью ctypes

Итак, скажем, код C/С++, который выделяет некоторую память, и возвращает указатель на него.

#include <stdlib.h>

#ifdef __cplusplus
  extern "C" {
#endif

void Allocate(void **p) {
 int N=2048;
 *p=malloc(N);
}

#ifdef __cplusplus
 }
#endif

Я ожидаю, что это моя обязанность освободить этот блок памяти, очевидно. Теперь предположим, что я скомпилирую это в разделяемую библиотеку и вызываю ее из Python с помощью ctypes, но не освобождаю явным образом эту память.

import ctypes
from ctypes import cdll, Structure, byref
external_lib = cdll.LoadLibrary('libtest.so.1.0')
ptr=ctypes.c_void_p(0)
external_lib.Allocate(ctypes.byref(ptr))

Если я запустил этот script с valgrind, я получаю утечку памяти из 2048 байт, если я скомпилирую test.cpp без флага "-O3". Но если я скомпилирую его с помощью флага -O3, то я не получаю утечку памяти.

Это не проблема - я всегда буду осторожно освобождать любую память, которую я выделяю. Но мне любопытно, откуда такое поведение.

Я протестировал это со следующим script в linux.

g++ -Wall -c -fPIC -fno-common test.cpp -o libtest1.o
g++ -shared -Wl,-soname,libtest1.so.1 -o libtest1.so.1.0  libtest1.o

g++ -O3 -Wall -c -fPIC -fno-common test.cpp -o libtest2.o
g++ -shared -Wl,-soname,libtest2.so.1 -o libtest2.so.1.0  libtest2.o

valgrind python test1.py &> report1
valgrind python test2.py &> report2

со следующим выходом

доклад1:

==27875== LEAK SUMMARY:
==27875==    definitely lost: 2,048 bytes in 1 blocks
==27875==    indirectly lost: 0 bytes in 0 blocks
==27875==      possibly lost: 295,735 bytes in 1,194 blocks
==27875==    still reachable: 744,633 bytes in 5,025 blocks
==27875==         suppressed: 0 bytes in 0 blocks

доклад2:

==27878== LEAK SUMMARY:
==27878==    definitely lost: 0 bytes in 0 blocks
==27878==    indirectly lost: 0 bytes in 0 blocks
==27878==      possibly lost: 295,735 bytes in 1,194 blocks
==27878==    still reachable: 746,681 bytes in 5,026 blocks
==27878==         suppressed: 0 bytes in 0 blocks

Ответы

Ответ 1

Похоже, что разные пользователи получают разные результаты в зависимости от своей платформы. Я попытался воспроизвести эту проблему безуспешно в системе Debian Wheezy с Python 2.5.5, Python 2.6.8, Python 3.2.3 с g++ 4.7.2.

Основываясь на вашем коде, вы знаете, что он протекает, это просто, что valgrind сообщает об использовании памяти по-разному. В отчете 1 определенно нет ссылки на кусок 2048. В отчете 2 он указан в разделе still reachable.

документация о дефектоскопии valgrind описывает, как обнаруживаются утечки. Интересно отметить, что он ищет ссылки как в памяти, так и в регистре общего назначения для каждого потока. Было бы возможно (но, я бы подумал, маловероятно), что, когда детектор течи запускается при выходе программы, по-прежнему сохраняется ссылка в одном из регистров процессора в выделенную память. Для неоптимизированной версии в функции Allocate могут существовать дополнительные инструкции, которые захватывают любую информацию о регистре, которая может содержать пропущенную ссылку. В оптимизированной версии функция Allocate позволяет сохранить ссылку в регистре, а также сохранить результат в *p.

Конечно, не имея возможности воспроизвести это, все это догадывается. Вы можете запросить valgrind для вывода дополнительной информации о найденных ссылках, которые могут обеспечить более полное представление о выделенных блоках.

например. Это покажет как доступные, так и недоступные блоки.

valgrind --show-reachable=yes --leak-check=full python2.5 test1.py &> report1-2.5

Если я изменю ваш код, чтобы быть следующим, все тесты в моей системе показывают, что блок 2048 определенно потерян (хотя было выделено 4096 байт). Это также заставляет меня думать, что это может быть какое-то значение кэшированного регистра, которое подбирается детектором утечки valgrind.

import ctypes
from ctypes import cdll, Structure, byref
external_lib = cdll.LoadLibrary('libtest.so.1.0')
ptr=ctypes.c_void_p(0)
external_lib.Allocate(ctypes.byref(ptr))
external_lib.Allocate(ctypes.byref(ptr))  # <-- Allocate a second block, the first becomes lost.

Здесь полученный фрагмент от valgrind показывает как доступный, так и недостижимый блок:

==28844== 2,048 bytes in 1 blocks are still reachable in loss record 305 of 366
==28844==    at 0x4C28BED: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==28844==    by 0x6CD870F: Allocate (in /projects/stack-overflow/18929183-python-garbage-collector-behavior-with-ctypes/libtest1.so.1.0)
==28844==    by 0x6ACEDEF: ffi_call_unix64 (in /usr/lib/python2.6/lib-dynload/_ctypes.so)
==28844==    by 0x6ACE86A: ffi_call (in /usr/lib/python2.6/lib-dynload/_ctypes.so)
==28844==    by 0x6AC9A66: _CallProc (callproc.c:816)
==28844==    by 0x6AC136C: CFuncPtr_call (_ctypes.c:3860)
==28844==    by 0x424989: PyObject_Call (abstract.c:2492)
==28844==    by 0x4A17B8: PyEval_EvalFrameEx (ceval.c:3968)
==28844==    by 0x49F0D1: PyEval_EvalCodeEx (ceval.c:3000)
==28844==    by 0x49F211: PyEval_EvalCode (ceval.c:541)
==28844==    by 0x4C66FE: PyRun_FileExFlags (pythonrun.c:1358)
==28844==    by 0x4C7A36: PyRun_SimpleFileExFlags (pythonrun.c:948)
==28844==
==28844== 2,048 bytes in 1 blocks are definitely lost in loss record 306 of 366
==28844==    at 0x4C28BED: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==28844==    by 0x6CD870F: Allocate (in /projects/stack-overflow/18929183-python-garbage-collector-behavior-with-ctypes/libtest1.so.1.0)
==28844==    by 0x6ACEDEF: ffi_call_unix64 (in /usr/lib/python2.6/lib-dynload/_ctypes.so)
==28844==    by 0x6ACE86A: ffi_call (in /usr/lib/python2.6/lib-dynload/_ctypes.so)
==28844==    by 0x6AC9A66: _CallProc (callproc.c:816)
==28844==    by 0x6AC136C: CFuncPtr_call (_ctypes.c:3860)
==28844==    by 0x424989: PyObject_Call (abstract.c:2492)
==28844==    by 0x4A17B8: PyEval_EvalFrameEx (ceval.c:3968)
==28844==    by 0x49F0D1: PyEval_EvalCodeEx (ceval.c:3000)
==28844==    by 0x49F211: PyEval_EvalCode (ceval.c:541)
==28844==    by 0x4C66FE: PyRun_FileExFlags (pythonrun.c:1358)
==28844==    by 0x4C7A36: PyRun_SimpleFileExFlags (pythonrun.c:948)

Ответ 2

Такое поведение исходит из оптимизаций gcc-O3. gcc видит, что выделенная память не используется и опускает этот блок кода.

Вы можете обратиться к этим вопросам: malloc и gcc optimization 2