├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── const.go ├── const_darwin.go ├── examples └── simple.go ├── experiment └── experiment.go ├── mmm.go └── mmm_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | *.gor 7 | 8 | # Folders 9 | _obj 10 | _test 11 | 12 | # Architecture specific extensions/prefixes 13 | *.[568vq] 14 | [568vq].out 15 | 16 | *.cgo1.go 17 | *.cgo2.c 18 | _cgo_defun.c 19 | _cgo_gotypes.go 20 | _cgo_export.* 21 | 22 | _testmain.go 23 | 24 | *.exe 25 | *.test 26 | 27 | 28 | *.sublime-workspace 29 | *.sw* 30 | *.un* 31 | 32 | app.conf.json 33 | docker.conf.json 34 | 35 | resources/ 36 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.4 4 | - 1.4.1 5 | - 1.4.2 6 | - 1.5 7 | - 1.5.1 8 | - tip 9 | script: 10 | - go test -v -race 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Clement 'cmc' Rey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mmm ![Status](https://img.shields.io/badge/status-stable-green.svg?style=plastic) [![Build Status](http://img.shields.io/travis/teh-cmc/mmm.svg?style=plastic)](https://travis-ci.org/teh-cmc/mmm) [![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=plastic)](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 ![License](https://img.shields.io/badge/license-MIT-blue.svg?style=plastic) 300 | 301 | The MIT License (MIT) - see LICENSE for more details 302 | 303 | Copyright (c) 2015 Clement 'cmc' Rey 304 | -------------------------------------------------------------------------------- /const.go: -------------------------------------------------------------------------------- 1 | // +build !darwin 2 | 3 | package mmm 4 | 5 | import "syscall" 6 | 7 | const ( 8 | mmapFlags = syscall.MAP_PRIVATE | syscall.MAP_ANONYMOUS 9 | ) 10 | -------------------------------------------------------------------------------- /const_darwin.go: -------------------------------------------------------------------------------- 1 | package mmm 2 | 3 | import "syscall" 4 | 5 | const ( 6 | mmapFlags = syscall.MAP_PRIVATE | syscall.MAP_ANON 7 | ) 8 | -------------------------------------------------------------------------------- /examples/simple.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2015 Clement 'cmc' Rey . 2 | // 3 | // Use of this source code is governed by an MIT-style 4 | // license that can be found in the LICENSE file. 5 | 6 | package main 7 | 8 | ///// 9 | // Simple example of usage 10 | // 11 | // go run examples/simple.go 12 | // 13 | ///// 14 | 15 | import ( 16 | "fmt" 17 | "log" 18 | "unsafe" 19 | 20 | "github.com/teh-cmc/mmm" 21 | ) 22 | 23 | type Coordinate struct { 24 | x, y int 25 | } 26 | 27 | func main() { 28 | // create a new memory chunk that contains 3 Coordinate structures 29 | mc, err := mmm.NewMemChunk(Coordinate{}, 3) 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | 34 | // print 3 35 | fmt.Println(mc.NbObjects()) 36 | 37 | // write {3,9} at index 0, then print {3,9} 38 | fmt.Println(mc.Write(0, Coordinate{3, 9})) 39 | // write {17,2} at index 1, then print {17,2} 40 | fmt.Println(mc.Write(1, Coordinate{17, 2})) 41 | // write {42,42} at index 2, then print {42,42} 42 | fmt.Println(mc.Write(2, Coordinate{42, 42})) 43 | 44 | // print {17,2} 45 | fmt.Println(mc.Read(1)) 46 | // print {42,42} 47 | fmt.Println(*((*Coordinate)(unsafe.Pointer(mc.Pointer(2))))) 48 | 49 | // free memory chunk 50 | if err := mc.Delete(); err != nil { 51 | log.Fatal(err) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /experiment/experiment.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2015 Clement 'cmc' Rey . 2 | // 3 | // Use of this source code is governed by an MIT-style 4 | // license that can be found in the LICENSE file. 5 | 6 | package main 7 | 8 | import ( 9 | "fmt" 10 | "log" 11 | "runtime" 12 | "runtime/debug" 13 | "time" 14 | "unsafe" 15 | 16 | "github.com/teh-cmc/mmm" 17 | ) 18 | 19 | // ----------------------------------------------------------------------------- 20 | 21 | ///// 22 | // This file shows various ways of using mmm's MemChunks and how these ways 23 | // affect GC's performances compared to native Go pointers. 24 | // 25 | // All of the results shown below were computed using a DELL XPS 15-9530 26 | // (i7-4712HQ@2.30GHz). 27 | // 28 | // go run experiment.go 29 | // 30 | ///// 31 | 32 | func main() { 33 | 34 | ///////////////////////////////////////////////// 35 | // A: Managed heap, 10 million pointers to int // 36 | ///////////////////////////////////////////////// 37 | fmt.Println(`Case A: what happens when we store 10 million pointers to integer 38 | on the managed heap?` + "\n") 39 | 40 | // build 10 million pointers to integer on the managed heap 41 | ints := make([]*int, 10*1e6) 42 | // init our pointers 43 | for i := range ints { 44 | j := i 45 | ints[i] = &j 46 | } 47 | 48 | // get rid of init garbage 49 | runtime.GC() 50 | debug.FreeOSMemory() 51 | 52 | for i := 0; i < 5; i++ { 53 | // randomly print one of our integers to make sure it's all working 54 | // as expected, and to prevent them from being optimized away 55 | fmt.Printf("\tvalue @ index %d: %d\n", i*1e4, *(ints[i*1e4])) 56 | 57 | // run GC 58 | now := time.Now().UnixNano() 59 | runtime.GC() 60 | fmt.Printf("\tGC time (managed heap, 10 million pointers): %d us\n", (time.Now().UnixNano()-now)/1e3) 61 | } 62 | 63 | // Results: 64 | // value @ index 0: 0 65 | // GC time (managed heap, 10 million pointers): 329840 us 66 | // value @ index 10000: 10000 67 | // GC time (managed heap, 10 million pointers): 325375 us 68 | // value @ index 20000: 20000 69 | // GC time (managed heap, 10 million pointers): 323367 us 70 | // value @ index 30000: 30000 71 | // GC time (managed heap, 10 million pointers): 327905 us 72 | // value @ index 40000: 40000 73 | // GC time (managed heap, 10 million pointers): 326469 us 74 | 75 | fmt.Println() 76 | 77 | ////////////////////////////////////////////////////// 78 | // B: Unmanaged heap, pointers generated on-the-fly // 79 | ////////////////////////////////////////////////////// 80 | fmt.Println(`Case B: mmm doesn't store any pointer, it doesn't need to. 81 | Since the data is stored on an unmanaged heap, it cannot be collected 82 | even if there's no reference to it. This allows mmm to generate 83 | pointers only when something's actually reading or writing to the data.` + "\n") 84 | 85 | // build 10 million integers on an unmanaged heap 86 | intz, err := mmm.NewMemChunk(int(0), 10*1e6) 87 | if err != nil { 88 | log.Fatal(err) 89 | } 90 | // init our integers 91 | for i := 0; i < int(intz.NbObjects()); i++ { 92 | intz.Write(i, i) 93 | } 94 | 95 | // get rid of (almost all) previous garbage 96 | ints = nil 97 | runtime.GC() 98 | debug.FreeOSMemory() 99 | 100 | for i := 0; i < 5; i++ { 101 | // randomly print one of our integers to make sure it's all working 102 | // as expected (pointer to data is generated on-the-fly) 103 | fmt.Printf("\tvalue @ index %d: %d\n", i*1e4, intz.Read(i*1e4)) 104 | 105 | // run GC 106 | now := time.Now().UnixNano() 107 | runtime.GC() 108 | fmt.Printf("\tGC time (unmanaged heap, pointers generated on-the-fly): %d us\n", (time.Now().UnixNano()-now)/1e3) 109 | } 110 | 111 | // Results: 112 | // value @ index 0: 0 113 | // GC time (unmanaged heap, pointers generated on-the-fly): 999 us 114 | // value @ index 10000: 10000 115 | // GC time (unmanaged heap, pointers generated on-the-fly): 665 us 116 | // value @ index 20000: 20000 117 | // GC time (unmanaged heap, pointers generated on-the-fly): 827 us 118 | // value @ index 30000: 30000 119 | // GC time (unmanaged heap, pointers generated on-the-fly): 882 us 120 | // value @ index 40000: 40000 121 | // GC time (unmanaged heap, pointers generated on-the-fly): 1016 us 122 | 123 | fmt.Println() 124 | 125 | /////////////////////////////////////////////////////// 126 | // C: Unmanaged heap, storing all generated pointers // 127 | /////////////////////////////////////////////////////// 128 | fmt.Println("Case C: what happens when we build and store 10 million pointers for each and every integer in our unmanaged memory chunk?\n") 129 | 130 | // build 10 million unsafe pointers on the managed heap 131 | ptrs := make([]unsafe.Pointer, 10*1e6) 132 | // init those pointers so that they point to the unmanaged heap 133 | for i := range ptrs { 134 | ptrs[i] = unsafe.Pointer(intz.Pointer(i)) 135 | } 136 | 137 | // get rid of (almost all) previous garbage 138 | runtime.GC() 139 | debug.FreeOSMemory() 140 | 141 | for i := 0; i < 5; i++ { 142 | // randomly print one of our integers to make sure it's all working 143 | // as expected 144 | fmt.Printf("\tvalue @ index %d: %d\n", i*1e4, *(*int)(ptrs[i*1e4])) 145 | 146 | // run GC 147 | now := time.Now().UnixNano() 148 | runtime.GC() 149 | fmt.Printf("\tGC time (unmanaged heap, all generated pointers stored): %d us\n", (time.Now().UnixNano()-now)/1e3) 150 | } 151 | 152 | // Results: 153 | // value @ index 0: 0 154 | // GC time (unmanaged heap, all generated pointers stored): 47196 us 155 | // value @ index 10000: 10000 156 | // GC time (unmanaged heap, all generated pointers stored): 47307 us 157 | // value @ index 20000: 20000 158 | // GC time (unmanaged heap, all generated pointers stored): 47485 us 159 | // value @ index 30000: 30000 160 | // GC time (unmanaged heap, all generated pointers stored): 47145 us 161 | // value @ index 40000: 40000 162 | // GC time (unmanaged heap, all generated pointers stored): 47221 us 163 | 164 | fmt.Println() 165 | 166 | /////////////////////////////////////////////////// 167 | // D: Unmanaged heap, storing numeric references // 168 | /////////////////////////////////////////////////// 169 | fmt.Println(`Case D: as case C showed, storing all the generated unsafe pointers to the unmanaged heap is still order of magnitudes faster than storing safe pointers to the managed heap. 170 | Still, why keep pointer values when our data is not garbage-collectable? 171 | What happens if we store all the generated pointers as numeric references?` + "\n") 172 | 173 | // build 10 million numeric references on the managed heap 174 | refs := make([]uintptr, 10*1e6) 175 | // init those references so that they each contain one of the addresses in 176 | // our unmanaged heap 177 | for i := range refs { 178 | refs[i] = uintptr(ptrs[i]) 179 | } 180 | 181 | // get rid of (almost all) previous garbage 182 | ptrs = nil 183 | runtime.GC() 184 | debug.FreeOSMemory() 185 | 186 | for i := 0; i < 5; i++ { 187 | // randomly print one of our integers to make sure it's all working 188 | // as expected 189 | fmt.Printf("\tvalue @ index %d: %d\n", i*1e4, *(*int)(unsafe.Pointer(refs[i*1e4]))) 190 | 191 | // run GC 192 | now := time.Now().UnixNano() 193 | runtime.GC() 194 | fmt.Printf("\tGC time (unmanaged heap, all numeric references stored): %d us\n", (time.Now().UnixNano()-now)/1e3) 195 | } 196 | 197 | // Results: 198 | // value @ index 0: 0 199 | // GC time (unmanaged heap, all numeric references stored): 715 us 200 | // value @ index 10000: 10000 201 | // GC time (unmanaged heap, all numeric references stored): 783 us 202 | // value @ index 20000: 20000 203 | // GC time (unmanaged heap, all numeric references stored): 882 us 204 | // value @ index 30000: 30000 205 | // GC time (unmanaged heap, all numeric references stored): 711 us 206 | // value @ index 40000: 40000 207 | // GC time (unmanaged heap, all numeric references stored): 723 us 208 | } 209 | -------------------------------------------------------------------------------- /mmm.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2015 Clement 'cmc' Rey . 2 | // 3 | // Use of this source code is governed by an MIT-style 4 | // license that can be found in the LICENSE file. 5 | 6 | package mmm 7 | 8 | import ( 9 | "fmt" 10 | "reflect" 11 | "runtime" 12 | "syscall" 13 | "unsafe" 14 | ) 15 | 16 | // ----------------------------------------------------------------------------- 17 | 18 | // Error represents an error within the mmm package. 19 | type Error string 20 | 21 | // Error implements the built-in error interface. 22 | func (e Error) Error() string { 23 | return string(e) 24 | } 25 | 26 | // ----------------------------------------------------------------------------- 27 | 28 | // TypeCheck recursively checks the underlying types of `v` and returns an error 29 | // if one or more of those types are illegal. 30 | func TypeCheck(i interface{}) error { 31 | v := reflect.ValueOf(i) 32 | if !v.IsValid() { 33 | return Error(fmt.Sprintf("unsuppported type: %#v", v)) 34 | } 35 | return typeCheck(v.Type()) 36 | } 37 | 38 | func typeCheck(t reflect.Type) error { 39 | switch k := t.Kind(); k { 40 | case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, 41 | reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, 42 | reflect.Uint32, reflect.Uint64, reflect.Uintptr, reflect.Float32, 43 | reflect.Float64, reflect.Complex64, reflect.Complex128, reflect.UnsafePointer: 44 | return nil 45 | case reflect.Array: 46 | return typeCheck(t.Elem()) 47 | case reflect.Struct: 48 | for i := 0; i < t.NumField(); i++ { 49 | if err := typeCheck(t.Field(i).Type); err != nil { 50 | return err 51 | } 52 | } 53 | return nil 54 | default: 55 | return Error(fmt.Sprintf("unsuppported type: %#v", k.String())) 56 | } 57 | } 58 | 59 | // ----------------------------------------------------------------------------- 60 | 61 | // MemChunk represents a chunk of manually allocated memory. 62 | type MemChunk struct { 63 | chunkSize uintptr 64 | objSize uintptr 65 | 66 | slice reflect.Value 67 | bytes []byte 68 | } 69 | 70 | // NbObjects returns the number of objects in the chunk. 71 | func (mc MemChunk) NbObjects() uint { 72 | return uint(mc.chunkSize / mc.objSize) 73 | } 74 | 75 | // ----------------------------------------------------------------------------- 76 | 77 | // Read returns the i-th object of the chunk as an interface. 78 | // 79 | // mmm doesn't provide synchronization of reads and writes on a MemChunk: it's 80 | // entirely up to you to decide how you want to manage thread-safety. 81 | // 82 | // This will panic if `i` is out of bounds. 83 | func (mc *MemChunk) Read(i int) interface{} { 84 | return mc.slice.Index(i).Interface() 85 | } 86 | 87 | // Write writes the passed value to the i-th object of the chunk. 88 | // 89 | // It returns the passed value. 90 | // 91 | // mmm doesn't provide synchronization of reads and writes on a MemChunk: it's 92 | // entirely up to you to decide how you want to manage thread-safety. 93 | // 94 | // This will panic if `i` is out of bounds, or if `v` is of a different type than 95 | // the other objects in the chunk. Or if anything went wrong. 96 | func (mc *MemChunk) Write(i int, v interface{}) interface{} { 97 | // panic if `v` is of a different type 98 | val := reflect.ValueOf(v) 99 | if val.Type() != mc.slice.Type().Elem() { 100 | panic("illegal value") 101 | } 102 | mc.slice.Index(i).Set(val) 103 | return v 104 | } 105 | 106 | // Pointer returns a pointer to the i-th object of the chunk. 107 | // 108 | // It returns uintptr instead of unsafe.Pointer so that code using mmm 109 | // cannot obtain unsafe.Pointers without importing the unsafe package 110 | // explicitly. 111 | // 112 | // This will panic if `i` is out of bounds. 113 | func (mc MemChunk) Pointer(i int) uintptr { 114 | return uintptr(unsafe.Pointer(&(mc.bytes[uintptr(i)*mc.objSize]))) 115 | } 116 | 117 | // ----------------------------------------------------------------------------- 118 | 119 | // NewMemChunk returns a new memory chunk. 120 | // 121 | // Supported types: 122 | // interfaces, 123 | // arrays, 124 | // structs, 125 | // numerics and boolean (bool/int/uint/float/complex and their variants), 126 | // unsafe.Pointer, 127 | // and any possible combination of the above. 128 | // 129 | // `v`'s memory representation will be used as a template for the newly 130 | // allocated memory. All data will be copied. 131 | // `n` is the number of `v`-like objects the memory chunk can contain (i.e., 132 | // sizeof(chunk) = sizeof(v) * n). 133 | func NewMemChunk(v interface{}, n uint) (MemChunk, error) { 134 | if n == 0 { 135 | return MemChunk{}, Error("`n` must be > 0") 136 | } 137 | if err := TypeCheck(v); err != nil { 138 | return MemChunk{}, err 139 | } 140 | 141 | t := reflect.TypeOf(v) 142 | size := t.Size() 143 | bytes, err := syscall.Mmap( 144 | 0, 0, int(size*uintptr(n)), 145 | syscall.PROT_READ|syscall.PROT_WRITE, 146 | mmapFlags, 147 | ) 148 | if err != nil { 149 | return MemChunk{}, err 150 | } 151 | 152 | // create a slice of type t, backed by the mmap'd memory 153 | itf := reflect.MakeSlice(reflect.SliceOf(t), int(n), int(n)).Interface() 154 | si := (*reflect.SliceHeader)((*[2]unsafe.Pointer)(unsafe.Pointer(&itf))[1]) 155 | si.Data = uintptr(unsafe.Pointer(&bytes[0])) 156 | 157 | // fill slice with copies of v 158 | slice := reflect.ValueOf(itf) 159 | for i := 0; i < slice.Len(); i++ { 160 | slice.Index(i).Set(reflect.ValueOf(v)) 161 | } 162 | 163 | ret := MemChunk{ 164 | chunkSize: size * uintptr(n), 165 | objSize: size, 166 | 167 | slice: slice, 168 | bytes: bytes, 169 | } 170 | 171 | // set a finalizer to free the chunk's memory when it would normally be 172 | // garbage collected 173 | runtime.SetFinalizer(&ret, func(chunk *MemChunk) { 174 | if chunk.bytes != nil { 175 | chunk.Delete() 176 | } 177 | }) 178 | 179 | return ret, nil 180 | } 181 | 182 | // Delete frees the memory chunk. 183 | func (mc *MemChunk) Delete() error { 184 | err := syscall.Munmap(mc.bytes) 185 | if err != nil { 186 | return err 187 | } 188 | 189 | mc.chunkSize = 0 190 | mc.objSize = 1 191 | mc.bytes = nil 192 | 193 | return nil 194 | } 195 | -------------------------------------------------------------------------------- /mmm_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2015 Clement 'cmc' Rey . 2 | // 3 | // Use of this source code is governed by an MIT-style 4 | // license that can be found in the LICENSE file. 5 | 6 | package mmm 7 | 8 | import ( 9 | "fmt" 10 | "log" 11 | "runtime" 12 | "testing" 13 | "unsafe" 14 | ) 15 | 16 | // ----------------------------------------------------------------------------- 17 | 18 | type Coordinate struct { 19 | x, y int 20 | } 21 | 22 | func Example_simple_usage() { 23 | // create a new memory chunk that contains 3 Coordinate structures 24 | mc, err := NewMemChunk(Coordinate{}, 3) 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | 29 | // print 3 30 | fmt.Println(mc.NbObjects()) 31 | 32 | // write {3,9} at index 0, then print {3,9} 33 | fmt.Println(mc.Write(0, Coordinate{3, 9})) 34 | // write {17,2} at index 1, then print {17,2} 35 | fmt.Println(mc.Write(1, Coordinate{17, 2})) 36 | // write {42,42} at index 2, then print {42,42} 37 | fmt.Println(mc.Write(2, Coordinate{42, 42})) 38 | 39 | // print {17,2} 40 | fmt.Println(mc.Read(1)) 41 | // print {42,42} 42 | fmt.Println(*((*Coordinate)(unsafe.Pointer(mc.Pointer(2))))) 43 | 44 | // free memory chunk 45 | if err := mc.Delete(); err != nil { 46 | log.Fatal(err) 47 | } 48 | 49 | // Output: 50 | // 3 51 | // {3 9} 52 | // {17 2} 53 | // {42 42} 54 | // {17 2} 55 | // {42 42} 56 | } 57 | 58 | // TestNewMemChunk tests the NewMemChunk function. 59 | func TestNewMemChunk(t *testing.T) { 60 | // can't create 0-length chunk 61 | _, err := NewMemChunk(nil, 0) 62 | if err.Error() != "`n` must be > 0" { 63 | t.Error("expected length error, got", err) 64 | } 65 | 66 | types := []interface{}{ 67 | // interface 68 | interface{}(0), 69 | // boolean 70 | false, 71 | // numeric 72 | byte(0), 73 | int(0), 74 | uint(0), 75 | uintptr(0), 76 | // array 77 | [3]int{}, 78 | // unsafe pointer 79 | unsafe.Pointer(new(int)), 80 | } 81 | for _, typ := range types { 82 | // create chunk of type 83 | mc, err := NewMemChunk(typ, 1) 84 | if err != nil { 85 | t.Fatal(err) 86 | } 87 | // Read should produce input value 88 | if mc.Read(0) != typ { 89 | t.Error(typ) 90 | } 91 | mc.Delete() 92 | } 93 | 94 | invalidTypes := []interface{}{ 95 | // nil pointer 96 | nil, 97 | // non-nil pointer 98 | new(int), 99 | // slice 100 | []int{}, 101 | // array of invalid type 102 | [3]*int{}, 103 | // struct with invalid type 104 | struct { 105 | valid int 106 | invalid *int 107 | }{}, 108 | } 109 | for _, typ := range invalidTypes { 110 | // create chunk of type 111 | _, err := NewMemChunk(typ, 1) 112 | if err == nil { 113 | t.Fatal("expected error, got nil") 114 | } 115 | } 116 | 117 | // run GC cycle; finalizers should run 118 | runtime.GC() 119 | } 120 | --------------------------------------------------------------------------------