Last active
January 17, 2025 14:03
-
-
Save timruffles/48bfa3bd5a625d80e7c73d0eacfc8335 to your computer and use it in GitHub Desktop.
golang - demonstrates the risk of using runtime.SetFinalizer to 'manage' C memory. Since C heap is invisible to Go GC, without sufficient Go memory pressure the finalizers won't run, and your process can hog a lot of memory. Suggestion: add a manual Close() method to your CGO bindings, so your library users can get predictable memory use.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import "C" | |
import ( | |
"fmt" | |
"runtime" | |
"sync" | |
"syscall" | |
"unsafe" | |
) | |
/* | |
#include <stdio.h> | |
#include <stdlib.h> | |
#include <string.h> | |
static int call_count = 0; | |
char* create_string(int alloc_size) | |
{ | |
call_count++; | |
char* str = (char*)malloc(alloc_size * sizeof(char)); | |
if (str == NULL) { | |
fprintf(stderr, "Memory allocation failed\n"); | |
exit(1); | |
} | |
snprintf(str, alloc_size, "Hello CGO! Call %d", call_count); | |
return str; | |
} | |
*/ | |
import "C" | |
const ( | |
kib = 1024 | |
mib = kib * 1024 | |
) | |
const ( | |
loopSize = 500 | |
allocPerIteration = mib * 2 | |
sampleSize = 100 | |
// whether it should force a Golang GC every sample | |
forceGc = false | |
) | |
var ( | |
rUsage syscall.Rusage | |
allocs int | |
frees int | |
lock sync.Mutex | |
) | |
func main() { | |
for sample := 1; sample <= sampleSize; sample++ { | |
for i := 0; i < loopSize; i++ { | |
a := NewAllocator() | |
if i == 0 && sample == 1 { | |
str := C.GoString(a.memory) | |
fmt.Println("test call: ", str) | |
fmt.Printf("sample\trss_mb\tallocs\tfrees\n") | |
} | |
} | |
if forceGc { | |
runtime.GC() | |
} | |
err := syscall.Getrusage(syscall.RUSAGE_SELF, &rUsage) | |
if err != nil { | |
panic(err) | |
} | |
lock.Lock() | |
// it's in bytes in OSX | |
mibUsed := rUsage.Maxrss / mib | |
fmt.Printf("%d\t%dMiB\t%d\t%d\n", sample, mibUsed, allocs, frees) | |
lock.Unlock() | |
} | |
} | |
type Allocator struct { | |
memory *C.char | |
} | |
func NewAllocator() *Allocator { | |
a := &Allocator{} | |
runtime.SetFinalizer(a, free) | |
a.memory = C.create_string(allocPerIteration) | |
allocs++ | |
return a | |
} | |
func free(a *Allocator) { | |
C.free(unsafe.Pointer(a.memory)) | |
a.memory = nil | |
frees++ | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment