github.com/teh-cmc/mmm@v0.0.0-20160717174312-f3d5d92d1c27/README.md (about) 1 # mmm  [](https://travis-ci.org/teh-cmc/mmm) [](http://godoc.org/github.com/teh-cmc/mmm) 2 3 Manual memory management for golang. 4 5 Have a look at [FreeTree](https://github.com/teh-cmc/freetree) for a real-world example of how to use `mmm`. 6 7 ## What you should definitely know.. 8 9 ## ..before using `mmm` 10 11 Go doesn't provide any manual memory management primitives. [**For very good reasons**](https://golang.org/doc/faq#garbage_collection). 12 This has been talked about numerous times on the [go-nuts mailing list](https://groups.google.com/forum/#!forum/golang-nuts), have a look over there for detailed discussions. 13 14 To make it short: **unless you are absolutely certain that you have no better alternative and that you understand all of the tradeoffs involved, please do not use this library.** 15 16 `mmm` is no black magic: it simply allocates memory segments outside of the GC-managed heap and provides a simple API (`Read()`, `Write()`, `Pointer()`) that abstracts away all of the evil stuff actually going on behind the scenes. 17 18 **The performances of Go's garbage collector depend heavily on the number of pointers your software is using.** 19 *No matter how much performance you gain by using `mmm`, you could have had the same gains had you redesigned your software to avoid the use of pointers entirely.* 20 21 This is the raison d'ĂȘtre of `mmm`: in some cases, purposefully (re)designing your software to avoid the use of pointers actually leads to code that is overly complex, harder to reason about, and thus, harder to maintain. In such cases, `mmm` might allow you to completely eliminate the GC overhead issues in your software, while keeping your original design (with minimal changes to your implementation, of course). 22 23 Note that `mmm` heavily relies on Go's implementation of interfaces. 24 25 Finally, for the adventurous, you'll find most of the ugly stuff [here](https://github.com/teh-cmc/mmm/blob/master/mmm.go#L44-L108) and [there](https://github.com/teh-cmc/mmm/blob/master/bytes.go#L50-L93). 26 27 UPDATE: this was discussed at length in [this HN thread](https://news.ycombinator.com/item?id=10649059). 28 29 ## ..once you've decided to use `mmm` 30 31 - Never point to data on the GC-managed heap using a pointer stored on an unmanaged heap. 32 33 If the only references to your garbage-collectable data are stored in an unmanaged memory chunk, and thus non-existent to the eyes of the GC, your data will be automatically deallocated. As it should be. 34 35 - `mmm` provides support for the following types: interfaces, arrays, structs, numerics/boolean (`bool`/`int`/`uint`/`float`/`complex` and their variants), `unsafe.Pointer`, and any possible combination of the above. 36 37 Slices and `string` are thus not supported, use arrays and byte arrays instead. 38 39 - `mmm` doesn't provide synchronization of reads and writes on a `MemChunk`. 40 41 It's entirely up to you to decide how you want to manage thread-safety. 42 43 ## Install 44 45 ```bash 46 go get -u github.com/teh-cmc/mmm 47 ``` 48 49 ## Example 50 51 Here's a simple example of usage (code [here](examples/simple.go)): 52 53 ```Go 54 package main 55 56 ///// 57 // Simple example of usage 58 // 59 // go run examples/simple.go 60 // 61 ///// 62 63 import ( 64 "fmt" 65 "log" 66 "unsafe" 67 68 "github.com/teh-cmc/mmm" 69 ) 70 71 type Coordinate struct { 72 x, y int 73 } 74 75 func main() { 76 // create a new memory chunk that contains 3 Coordinate structures 77 mc, err := mmm.NewMemChunk(Coordinate{}, 3) 78 if err != nil { 79 log.Fatal(err) 80 } 81 82 // print 3 83 fmt.Println(mc.NbObjects()) 84 85 // write {3,9} at index 0, then print {3,9} 86 fmt.Println(mc.Write(0, Coordinate{3, 9})) 87 // write {17,2} at index 1, then print {17,2} 88 fmt.Println(mc.Write(1, Coordinate{17, 2})) 89 // write {42,42} at index 2, then print {42,42} 90 fmt.Println(mc.Write(2, Coordinate{42, 42})) 91 92 // print {17,2} 93 fmt.Println(mc.Read(1)) 94 // print {42,42} 95 fmt.Println(*((*Coordinate)(unsafe.Pointer(mc.Pointer(2))))) 96 97 // free memory chunk 98 if err := mc.Delete(); err != nil { 99 log.Fatal(err) 100 } 101 } 102 ``` 103 104 ## Demonstration 105 106 Complete code for the following demonstration is available [here](experiment/experiment.go). 107 108 All of the results shown below were computed using a DELL XPS 15-9530 (i7-4712HQ@2.30GHz). 109 110 #### Case A: managed heap, 10 million pointers to int 111 112 Let's see what happens when we store 10 millions pointers to integer on the managed heap: 113 114 ```Go 115 // build 10 million pointers to integer on the managed heap 116 ints := make([]*int, 10*1e6) 117 // init our pointers 118 for i := range ints { 119 j := i 120 ints[i] = &j 121 } 122 123 for i := 0; i < 5; i++ { 124 // randomly print one of our integers to make sure it's all working 125 // as expected, and to prevent them from being optimized away 126 fmt.Printf("\tvalue @ index %d: %d\n", i*1e4, *(ints[i*1e4])) 127 128 // run GC 129 now := time.Now().UnixNano() 130 runtime.GC() 131 fmt.Printf("\tGC time (managed heap, 10 million pointers): %d us\n", (time.Now().UnixNano()-now)/1e3) 132 } 133 ``` 134 135 This prints: 136 137 ``` 138 value @ index 0: 0 139 GC time (managed heap, 10 million pointers): 329840 us 140 value @ index 10000: 10000 141 GC time (managed heap, 10 million pointers): 325375 us 142 value @ index 20000: 20000 143 GC time (managed heap, 10 million pointers): 323367 us 144 value @ index 30000: 30000 145 GC time (managed heap, 10 million pointers): 327905 us 146 value @ index 40000: 40000 147 GC time (managed heap, 10 million pointers): 326469 us 148 ``` 149 150 That's an average ~326ms per GC call. 151 Let's move to case B where we will start using `mmm`'s memory chunks. 152 153 #### Case B: unmanaged heap, pointers generated on-the-fly 154 155 `mmm` doesn't store any pointer, it doesn't need to. 156 157 Since the data is stored on an unmanaged heap, it cannot be collected even if there's no reference to it. This allows `mmm` to generate pointers only when something's actually reading or writing to the data. 158 159 In pratice, it looks like that: 160 161 ```Go 162 // build 10 million integers on an unmanaged heap 163 intz, err := mmm.NewMemChunk(int(0), 10*1e6) 164 if err != nil { 165 log.Fatal(err) 166 } 167 // init our integers 168 for i := 0; i < int(intz.NbObjects()); i++ { 169 intz.Write(i, i) 170 } 171 172 for i := 0; i < 5; i++ { 173 // randomly print one of our integers to make sure it's all working 174 // as expected (pointer to data is generated on-the-fly) 175 fmt.Printf("\tvalue @ index %d: %d\n", i*1e4, intz.Read(i*1e4)) 176 177 // run GC 178 now := time.Now().UnixNano() 179 runtime.GC() 180 fmt.Printf("\tGC time (unmanaged heap, pointers generated on-the-fly): %d us\n", (time.Now().UnixNano()-now)/1e3) 181 ``` 182 183 This prints: 184 185 ``` 186 value @ index 0: 0 187 GC time (unmanaged heap, pointers generated on-the-fly): 999 us 188 value @ index 10000: 10000 189 GC time (unmanaged heap, pointers generated on-the-fly): 665 us 190 value @ index 20000: 20000 191 GC time (unmanaged heap, pointers generated on-the-fly): 827 us 192 value @ index 30000: 30000 193 GC time (unmanaged heap, pointers generated on-the-fly): 882 us 194 value @ index 40000: 40000 195 GC time (unmanaged heap, pointers generated on-the-fly): 1016 us 196 ``` 197 198 That's an average ~0.9ms per GC call. 199 200 We went from a ~326ms average to a ~0.9ms average; but the comparison isn't really fair now, is it? In case A we were storing every pointer, here we're simply not storing any. 201 202 That leads us to case C, in which we build pointers to each and every integer that's in our unmanaged heap. 203 204 #### Case C: unmanaged heap, storing all generated pointers 205 206 What happens when we build and store 10 million pointers: one for each and every integer that's in our unmanaged memory chunk? 207 208 ```Go 209 // build 10 million unsafe pointers on the managed heap 210 ptrs := make([]unsafe.Pointer, 10*1e6) 211 // init those pointers so that they point to the unmanaged heap 212 for i := range ptrs { 213 ptrs[i] = unsafe.Pointer(intz.Pointer(i)) 214 } 215 216 for i := 0; i < 5; i++ { 217 // randomly print one of our integers to make sure it's all working 218 // as expected 219 fmt.Printf("\tvalue @ index %d: %d\n", i*1e4, *(*int)(ptrs[i*1e4])) 220 221 // run GC 222 now := time.Now().UnixNano() 223 runtime.GC() 224 fmt.Printf("\tGC time (unmanaged heap, all generated pointers stored): %d us\n", (time.Now().UnixNano()-now)/1e3) 225 } 226 ``` 227 228 This prints: 229 230 ``` 231 value @ index 0: 0 232 GC time (unmanaged heap, all generated pointers stored): 47196 us 233 value @ index 10000: 10000 234 GC time (unmanaged heap, all generated pointers stored): 47307 us 235 value @ index 20000: 20000 236 GC time (unmanaged heap, all generated pointers stored): 47485 us 237 value @ index 30000: 30000 238 GC time (unmanaged heap, all generated pointers stored): 47145 us 239 value @ index 40000: 40000 240 GC time (unmanaged heap, all generated pointers stored): 47221 us 241 ``` 242 243 The results here are pretty interesting: on the one hand this is ~47 times slower than case B (in which we used `mmm`'s memory chunks but didn't actually store any pointer), but on the other hand this is still 6 times faster than case A (in which we used native pointers) because unsafe pointers require far less work from the GC. 244 245 Six times faster is already quite the good deal, but why stop there? As we've already pointed out, `mmm` doesn't need to store references to its data... so.. don't. 246 247 This is what case D is all about, in which we will convert those pointers into simple numeric references and store them as such. 248 249 #### Case D: unmanaged heap, storing numeric references 250 251 Instead of storing (unsafe) pointers, let's treat these pointers as what they really are: simple numeric references. 252 253 ```Go 254 // build 10 million numeric references on the managed heap 255 refs := make([]uintptr, 10*1e6) 256 // init those references so that they each contain one of the addresses in 257 // our unmanaged heap 258 for i := range refs { 259 refs[i] = uintptr(ptrs[i]) 260 } 261 262 // get rid of those unsafe pointers we stored 263 ptrs = nil 264 265 for i := 0; i < 5; i++ { 266 // randomly print one of our integers to make sure it's all working 267 // as expected 268 fmt.Printf("\tvalue @ index %d: %d\n", i*1e4, *(*int)(unsafe.Pointer(refs[i*1e4]))) 269 270 // run GC 271 now := time.Now().UnixNano() 272 runtime.GC() 273 fmt.Printf("\tGC time (unmanaged heap, all numeric references stored): %d us\n", (time.Now().UnixNano()-now)/1e3) 274 } 275 ``` 276 277 This prints: 278 279 ``` 280 value @ index 0: 0 281 GC time (unmanaged heap, all numeric references stored): 715 us 282 value @ index 10000: 10000 283 GC time (unmanaged heap, all numeric references stored): 783 us 284 value @ index 20000: 20000 285 GC time (unmanaged heap, all numeric references stored): 882 us 286 value @ index 30000: 30000 287 GC time (unmanaged heap, all numeric references stored): 711 us 288 value @ index 40000: 40000 289 GC time (unmanaged heap, all numeric references stored): 723 us 290 ``` 291 292 We're basically back to the results of case B. 293 As far as the GC is concerned, those pointers don't exist, which translates into sub-millisecond GC calls. 294 295 Still, the memory they point to does exist, and is just one cast away from being read from and written to. 296 297 We now have everything we need to build pointer-based software without any GC overhead, and without any design modification: this is basically how [FreeTree](https://github.com/teh-cmc/freetree) is implemented. 298 299 ## License  300 301 The MIT License (MIT) - see LICENSE for more details 302 303 Copyright (c) 2015 Clement 'cmc' Rey <cr.rey.clement@gmail.com>