On Sat, May 23, 2020 at 5:19 PM Justin Israel <justinisr...@gmail.com> wrote:
> I've been working to track this down for 2 days now and I'm just taking a > long shot to see if anyone might have a new idea for me. > My cgo-based bindings library seems to have unbounded RSS memory growth, > which I have been able to reduce to the smallest benchmark test and even > pinpoint the exact call, but the reason behind it still eludes me. > The context is that I have a struct in C++ that will store a const char* > for the last exception that was thrown, as a strdup() copy which gets > cleaned up properly each time. > > typedef struct _HandleContext { > HandleId handle; > const char* last_error; > } _HandleContext; > const char* getLastError(_HandleContext* ctx); > > On the Go side, I have a function for lastError() to return the last error > value > > func (c *Config) lastError() error { > err := C.getLastError(c.ptr) > if err == nil { > return nil > } > e := C.GoString(err) > if e == "" { > return nil > } > runtime.KeepAlive(c) > // return nil // <- would result in no memory growth > return errors.New(e) // <- results in memory growth > } > > What I am seeing in my benchmark test is that the RSS grows something like > 20MB a second, yet the GODEBUG=gctrace=1 and the pprof memory profile don't > reflect this memory usage at all, aside from showing a hotspot (in pprof) > being the GoString() call: > > gc 4 @4.039s 0%: 0.006+0.14+0.003 ms clock, 0.024+0.10/0.039/0.070+0.014 ms > cpu, 4->4->0 MB, 5 MB goal, 4 P > gc 5 @6.857s 0%: 0.003+0.20+0.004 ms clock, 0.015+0.069/0.025/0.15+0.016 ms > cpu, 4->4->0 MB, 5 MB goal, 4 P > ... > gc 26 @69.498s 0%: 0.005+0.12+0.003 ms clock, 0.021+0.10/0.044/0.093+0.014 ms > cpu, 4->4->0 MB, 5 MB goal, 4 P > // 800MB RSS usage > gc 27 @71.824s 0%: 0.006+2.2+0.003 ms clock, 0.025+0.063/0.058/0.11+0.014 ms > cpu, 4->4->0 MB, 5 MB goal, 4 P > > (pprof) top10 > Showing nodes accounting for 46083.69kB, 100% of 46083.69kB total > Showing top 10 nodes out of 19 > flat flat% sum% cum cum% > 30722.34kB 66.67% 66.67% 30722.34kB 66.67% <...>._Cfunc_GoStringN (inline) > 7168.11kB 15.55% 82.22% 7168.11kB 15.55% errors.New (inline) > 3073.16kB 6.67% 88.89% 46083.69kB 100% <...>.testLeak > 1536.02kB 3.33% 92.22% 1536.02kB 3.33% > <...>.(*DisplayTransform).SetInputColorSpace.func1 > 1024.02kB 2.22% 94.44% 1024.02kB 2.22% <...>.(*Config).NumViews.func1 > 1024.02kB 2.22% 96.67% 1024.02kB 2.22% <...>.(*Config).View.func1 > 512.01kB 1.11% 97.78% 512.01kB 1.11% > <...>.(*DisplayTransform).SetView.func1 > 512.01kB 1.11% 98.89% 512.01kB 1.11% <...>._Cfunc_GoString (inline) > 512.01kB 1.11% 100% 512.01kB 1.11% <...>.newProcessor (inline) > 0 0% 100% 512.01kB 1.11% > <...>.(*Config).ColorSpaceNameByIndex > > Regardless of whether I ignore the error return value in my test, it > grows. If I return nil instead of errors.New(e), it will stay around 20MB > RSS. > > I MUST be doing something stupid, but I can't see any reason for the > memory growth based on returning this string wrapped in an error. At first > I thought I was leaking in C/C++ but it a led to this one factor on the Go > side. Any tips would help greatly, since I have tried debug GC output, > pprof reports, valgrind, address sanitizer, and refactoring the entire > memory management of my C bindings layer. > > Justin > > Well I seem to have resolved the leak, which was due to a poor assumption on my part about the frequency of finalizer execution and being tied to a GC cycle. Are finalizers executed on every GC cycle, or are they maybe executed on a GC cycle but not sooner than? My library creates finalizers to ensure C memory is freed at some point, but I also have Destroy() methods to immediately free them (and clear finalizers). So I think it was a combination of the test not generating enough Go garbage to clean up the more significant C memory, and not being explicit enough with Destroy calls. I look to have also had a situation where I wasn't cleaning up C strings as fast as I could have been, so that also helps to clear them more quickly before a Destroy or a finalizer runs. As much as I thought I knew about the caveats of finalizers and using them to release C resources, I still likely made faulty assumptions. -- You received this message because you are subscribed to the Google Groups "golang-nuts" group. To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts+unsubscr...@googlegroups.com. To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/CAPGFgA0Q6jFovOhjHxo0vNFSvqUaBPC_uU3sJhL5LnDbmQM1%2BA%40mail.gmail.com.