2013-09-21 11 views
15

Załóżmy, że mam kod C/C++, który przydziela trochę pamięci i zwraca do niej wskaźnik.Python: zachowanie śmieciarki z ctypami

#include <stdlib.h> 

#ifdef __cplusplus 
    extern "C" { 
#endif 

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

#ifdef __cplusplus 
} 
#endif 

Oczekuję, że moim obowiązkiem jest oczywiście uwolnienie tego bloku pamięci. Teraz przypuśćmy, że skompiluję to do biblioteki współdzielonej i wywołasz ją z Pythona za pomocą ctypes, ale nie uwalniaj jawnie tej pamięci.

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)) 

Jeśli uruchomić ten skrypt z Valgrind, pojawia się przeciek pamięci 2048 bajtów jeśli skompilować test.cpp bez flagi „-O3”. Ale jeśli skompiluję go z flagą "-O3", to nie dostanę przecieku pamięci.

To naprawdę nie problem - zawsze będę ostrożny, aby jawnie zwolnić pamięć, którą przydzielam. Ale jestem ciekawy, skąd to zachowanie pochodzi.

Testowałem to za pomocą następującego skryptu w Linuksie.

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 

z następującym wyjściu

raportu1:

==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 

sprawozdania2:

==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

Mam następnie swoje kroki i wyniki są interesujące. W "Pythonie 3.3.2" oba raporty dają przecieki z 2048 bajtami, jednak w "Pythonie 2.7.5" nie ma wycieku z żadnym z raportów. Testowany w 'Linux 3.11.4 x86_64' z' gcc 4.8.1 20130725' – starrify

Odpowiedz

-1

Takie zachowanie pochodzą z gcc optymalizacji -O3. gcc widzi, że przydzielona pamięć jest nieużywana i pomija ten blok kodu.

Można odwołać się do tego pytania: malloc and gcc optimization 2

+0

Nie sądzę, że tak jest w tym przypadku. W pytaniu, które podasz, przydzielony adres nigdy nie opuszcza pętli i nigdy nie jest używany. Tutaj adres jest gdzieś zapisany za pomocą wskaźnika argumentu, więc nie można go oznaczyć jako "nieużywany". – viraptor

+0

-1 dla oczywiście niepoprawnej odpowiedzi. Przynajmniej spróbuj zdemontować plik binarny lub zobacz wynik 'gcc -S', aby sprawdzić, czy kod zawierający' malloc' zostanie wyeliminowany. – starrify

4

Różni użytkownicy wydają się uzyskać różne wyniki w zależności od platformy. Próbowałem bez powodzenia odtworzyć ten problem w systemie Debian Wheezy z Python 2.5.5, Python 2.6.8, Python 3.2.3 z g ++ 4.7.2.

Na podstawie Twojego kodu wiadomo, że jest nieszczelny, to tylko, że valgrind raportuje użycie pamięci w inny sposób. W raporcie 1 zdecydowanie nie ma odniesienia do porcji 2048. W raporcie 2 wymieniono go w sekcji still reachable.

W artykule valgrind leak detector documentation opisano sposób wykrywania wycieków. Warto zauważyć, że szuka odniesień w pamięci i rejestrze ogólnego przeznaczenia dla każdego wątku. Byłoby możliwe (ale wydawałoby mi się to nieprawdopodobne), że gdy detektor wycieków uruchomi się po wyjściu z programu, w jednym z rejestrów CPU nadal znajduje się odniesienie do pamięci, która została przydzielona. W przypadku niezoptymalizowanej wersji mogą istnieć dodatkowe instrukcje w funkcji Allocate, które przejmują wszelkie informacje rejestru, które mogą zawierać wyciek odwołań. W wersji zoptymalizowanej funkcja Allocate zachowuje referencję w rejestrze, a także zapisuje wynik w postaci *p.

Oczywiście, bez możliwości odtworzenia tego, to wszystko jest zgadywaniem. Możesz poprosić o numer valgrind, aby uzyskać więcej informacji na temat znalezionych referencji, które mogą zapewnić lepszy wgląd w przydzielone bloki.

np. Spowoduje to wyświetlenie zarówno osiągalnych, jak i nieosiągalnych bloków.

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

Gdybym zmodyfikować kod, aby być dodaje, wszystkie testy na moim systemie wskazują, że 2048 blok jest zdecydowanie stracił (choć 4096 bajtów zostały przydzielone). To również prowadzi mnie do przekonania, że ​​może to być pewnego rodzaju buforowana wartość rejestru, która jest odbierana przez wykrywacz nieszczelności 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. 

Oto fragment uzyskany z valgrind pokazując zarówno osiągalny i nieosiągalny bloku:

==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)