A few days ago I noticed that my game uses huge amounts of memory. More than 1GB of RAM was allocated for just a small scenery. Since my engine is not that fancy it is unlikely that it would actually require such a high amount of memory. So I started to investigate. The problem could mainly have two sources. The first being that simply more memory is allocated than needed, the other one is that memory is allocated but never freed again, slowly filling up the ram with unused data relics. The second one is also called a ‘memory leak’.
To solve this, I could dig through the source code and check every call to malloc, realloc or free for potential errors. But this task is very tedious and it becomes nearly impossible to find memory leaks in big and complicated applications by hand.
So, instead I came up with an idea to locate such problems more easily and with technical assistance. Basically, I wrote a few wrappers for the original memory management functions and logged every call, including it’s parameters and return value, into a file.
For every source code file in the project I include the following lines in the header:
#define MEMORY_LOGGER_ACTIVE
void* _memLogger_malloc(int size, char* file, sint32 line);
void* _memLogger_realloc(void* old, int size, char* file, sint32 line);
void _memLogger_free(void* p, char* file, sint32 line);
#ifdef MEMORY_LOGGER_ACTIVE
#define malloc(x) _memLogger_malloc(x,__FILE__,__LINE__)
#define realloc(x,y) _memLogger_realloc(x,y,__FILE__,__LINE__)
#define free(x) _memLogger_free(x,__FILE__,__LINE__)
#endif
As you can see, it will redirect every call to malloc, realloc and free to another function and will also pass the sourcecode filename and linenumber of the call as parameters.
Additionally to the code above, it is required to implement the three wrapper methods somewhere.
I suggest to log the filename, linenumber and buffer size and the address returned (in case of malloc and realloc) and all parameters to a file. Don’t forget to call #undef for all three method names right before the wrappers to avoid recursive calling.
Why write to a file and not analyze the data immediately? This has several advantages. First of all, it is possible to keep memory logs for later usage or even deliver builds that have memory logging activated to your end-users. Some memory leaks only eat up RAM very slowly, which means that you have to run the application for several hours. Another advantage is that you can avoid calling many functions in the wrappers which will not only slow down the program, but also, if the external methods allocate memory, will end you up crashing because of endless recursive calls.
So far so good, now run your application, do some stuff while every memory operation is logged. If you use a small cache and do not write to the file immediately you shouldn’t even notice any slowdown. Anyways, after that you need to analyze the log. I wrote a simple application that keeps track of memory buffers and their size and where they have been allocated (filename + linenumber).
At the end I simply group them by filename/linenumber, count them and return a list, sorted by amount of total memory used.
Here is the final output for my game: (no censoring of super secret file names!)
Size: 026642400b (0025mb) Count: 01275 - .\eisland.cpp(584)
Size: 008388608b (0008mb) Count: 00001 - .\maineditor.cpp(270)
Size: 005328480b (0005mb) Count: 01275 - .\eisland.cpp(698)
Size: 005242880b (0005mb) Count: 00001 - .\eislandbuilder.cpp(19)
Size: 000698368b (0000mb) Count: 00073 - .\geodetailbuffer.cpp(68)
Size: 000400400b (0000mb) Count: 00091 - .\jhlib\gfx\ematerialloader.cpp(60)
Size: 000191744b (0000mb) Count: 00012 - .\geobuffer.cpp(137)
Size: 000129600b (0000mb) Count: 00003 - .\wavefront.cpp(397)
Size: 000092448b (0000mb) Count: 01197 - .\eisland.cpp(885)
Size: 000088064b (0000mb) Count: 00172 - .\jhlib\sdata.cpp(13)
Size: 000071624b (0000mb) Count: 01279 - .\jhlib\gfx\egeometry.cpp(303)
Size: 000071400b (0000mb) Count: 01275 - .\jhlib\gfx\egeometry.cpp(98)
Size: 000064512b (0000mb) Count: 00256 - .\maineditor.cpp(286)
Size: 000052160b (0000mb) Count: 00042 - .\jhlib\gfx\ui\eui_font.cpp(217)
Size: 000033704b (0000mb) Count: 01041 - .\jhlib\simplelist.cpp(10)
Size: 000030672b (0000mb) Count: 01278 - .\jhlib\gfx\egeometry.cpp(765)
Size: 000024576b (0000mb) Count: 00004 - .\eworld.cpp(61)
Size: 000016656b (0000mb) Count: 01041 - .\jhlib\simplelist.cpp(5)
Size: 000014400b (0000mb) Count: 00001 - .\editorcursorarea.cpp(126)
Size: 000012288b (0000mb) Count: 00001 - .\esky.cpp(55)
Size: 000011284b (0000mb) Count: 00091 - .\jhlib\gfx\ematerialloader.cpp(8)
Size: 000008192b (0000mb) Count: 00004 - .\eworld.cpp(79)
Size: 000007168b (0000mb) Count: 00001 - .\jhlib\gfx\ui\eui_font.cpp(12)
Size: 000005152b (0000mb) Count: 01278 - .\jhlib\gfx\egeometry.cpp(768)
Size: 000004112b (0000mb) Count: 00001 - .\eworld.cpp(7)
Size: 000003432b (0000mb) Count: 00066 - .\jhlib\gfx\eshader.cpp(850)
Size: 000003072b (0000mb) Count: 00003 - .\jhlib\gfx\ui\eui_textbox.cpp(47)
Size: 000002560b (0000mb) Count: 00001 - .\esky.cpp(54)
Size: 000002496b (0000mb) Count: 00039 - .\jhlib\gfx\ui\eui_label.cpp(23)
Size: 000002016b (0000mb) Count: 00042 - .\jhlib\gfx\ui\eui_font.cpp(84)
Size: 000001536b (0000mb) Count: 00024 - .\jhlib\gfx\eshader.cpp(1173)
Size: 000001460b (0000mb) Count: 00073 - .\geodetailbuffer.cpp(62)
Size: 000001404b (0000mb) Count: 00039 - .\jhlib\gfx\ui\eui_label.cpp(8)
Size: 000001344b (0000mb) Count: 00084 - .\jhlib\gfx\etexture.cpp(99)
Size: 000001152b (0000mb) Count: 00024 - .\geobuffer.cpp(304)
Size: 000001024b (0000mb) Count: 00001 - .\maineditor.cpp(265)
Size: 000000700b (0000mb) Count: 00025 - .\wavefrontimportdetailed.cpp(92)
Size: 000000672b (0000mb) Count: 00012 - .\geobuffer.cpp(263)
Size: 000000400b (0000mb) Count: 00001 - .\emodelstructure.cpp(39)
Size: 000000336b (0000mb) Count: 00007 - .\geobuffer.cpp(19)
Size: 000000294b (0000mb) Count: 00013 - .\jhlib\sdata.cpp(158)
Size: 000000288b (0000mb) Count: 00003 - .\jhlib\gfx\ui\eui_textbox.cpp(10)
Size: 000000280b (0000mb) Count: 00005 - .\jhlib\gfx\egeometry.cpp(56)
Size: 000000252b (0000mb) Count: 00001 - .\emodelstructure.cpp(18)
Size: 000000228b (0000mb) Count: 00003 - .\jhlib\gfx\ui\eui_frame.cpp(8)
Size: 000000224b (0000mb) Count: 00004 - .\jhlib\gfx\egeometry.cpp(77)
Size: 000000204b (0000mb) Count: 00003 - .\maineditor.cpp(314)
Size: 000000192b (0000mb) Count: 00003 - .\jhlib\gfx\ui\eui_frame.cpp(30)
Size: 000000192b (0000mb) Count: 00003 - .\jhlib\gfx\ui\eui_textbox.cpp(60)
Size: 000000168b (0000mb) Count: 00006 - .\jhlib\streamwrapper.cpp(5)
Size: 000000168b (0000mb) Count: 00006 - .\wavefrontimportdetailed.cpp(109)
Size: 000000120b (0000mb) Count: 00006 - .\jhlib\sdata.cpp(38)
Size: 000000120b (0000mb) Count: 00006 - .\wavefrontimportdetailed.cpp(104)
Size: 000000117b (0000mb) Count: 00013 - .\jhlib\sdata.cpp(144)
Size: 000000104b (0000mb) Count: 00006 - .\jhlib\sdata.cpp(95)
Size: 000000080b (0000mb) Count: 00004 - .\eitem.cpp(18)
Size: 000000064b (0000mb) Count: 00008 - .\jhlib\gfx\euiiconatlas.cpp(32)
Size: 000000060b (0000mb) Count: 00001 - .\maineditor.cpp(251)
Size: 000000056b (0000mb) Count: 00001 - .\jhlib\gfx\egeometry.cpp(33)
Size: 000000048b (0000mb) Count: 00003 - .\wavefront.cpp(97)
Size: 000000048b (0000mb) Count: 00001 - .\eislandbuilder.cpp(11)
Size: 000000048b (0000mb) Count: 00006 - .\jhlib\filemgr.cpp(21)
Size: 000000040b (0000mb) Count: 00001 - .\jhlib\gfx\ecamera.cpp(510)
Size: 000000040b (0000mb) Count: 00001 - .\geobuffer.cpp(247)
Size: 000000036b (0000mb) Count: 00001 - .\jhlib\gfx\eshader.cpp(57)
Size: 000000036b (0000mb) Count: 00001 - .\emodelstructure.cpp(7)
Size: 000000032b (0000mb) Count: 00001 - .\jhlib\simplelist.cpp(21)
Size: 000000024b (0000mb) Count: 00003 - .\engine.cpp(295)
Size: 000000020b (0000mb) Count: 00001 - .\geobuffer.cpp(192)
Size: 000000020b (0000mb) Count: 00001 - .\wavefront.cpp(94)
Size: 000000020b (0000mb) Count: 00001 - .\wavefrontimportdetailed.cpp(827)
Size: 000000020b (0000mb) Count: 00001 - .\jhlib\gfx\euiiconatlas.cpp(42)
Size: 000000020b (0000mb) Count: 00001 - .\eitem.cpp(28)
Size: 000000016b (0000mb) Count: 00001 - .\jhlib\gfx\ui\eui_font.cpp(8)
Size: 000000016b (0000mb) Count: 00001 - .\jhlib\gfx\euiiconatlas.cpp(13)
Size: 000000016b (0000mb) Count: 00001 - .\eitemblueprints.cpp(28)
Size: 000000012b (0000mb) Count: 00001 - .\eitemblueprints.cpp(7)
Size: 000000012b (0000mb) Count: 00003 - .\jhlib\gfx\ui\eui_textbox.cpp(44)
Size: 000000012b (0000mb) Count: 00003 - .\jhlib\gfx\ui\eui_textbox.cpp(41)
At the beginning of the post, I spoke of >1000MB memory used. Regrading to the logs it should be somewhere at 100-200MB (note that I round down the amount of memory used, so even 987KB would become 0MB in the statistics)
That is because I stopped logging right after the game booted up. No map data was loaded at all. Still, I could locate a big memory waste easily. In the very first line it tells me that 25MB are allocated in eIsland.cpp on line 584. After checking the code on the given location I noticed that, due to a bug, a lot more memory was allocated than really needed.
Thanks to this approach I got my game down from 1GB to 100MB memory consumption.