├── .gitignore ├── Makefile ├── Readme.md ├── benchmark.py ├── build-all.sh ├── clean.sh ├── gc-latencies-zoomed-in.svg ├── gc-latencies.svg ├── requirements.txt └── src ├── go └── main.go ├── haskell └── Main.hs ├── java └── Main.java ├── node ├── main-immutable.js ├── main.js ├── package.json └── yarn.lock ├── ocaml ├── _tags ├── main.ml └── parse_ocaml_log.ml ├── python ├── __init__.py └── main.py ├── racket └── main.rkt ├── scala └── Main.scala └── swift └── Main.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 5 | 6 | # User-specific stuff: 7 | .idea/workspace.xml 8 | .idea/tasks.xml 9 | .idea/dictionaries 10 | .idea/vcs.xml 11 | .idea/jsLibraryMappings.xml 12 | 13 | # Sensitive or high-churn files: 14 | .idea/dataSources.ids 15 | .idea/dataSources.xml 16 | .idea/dataSources.local.xml 17 | .idea/sqlDataSources.xml 18 | .idea/dynamic.xml 19 | .idea/uiDesigner.xml 20 | 21 | # Gradle: 22 | .idea/gradle.xml 23 | .idea/libraries 24 | 25 | # Mongo Explorer plugin: 26 | .idea/mongoSettings.xml 27 | 28 | ## File-based project format: 29 | *.iws 30 | 31 | ## Plugin-specific files: 32 | 33 | # IntelliJ 34 | /out/ 35 | 36 | # mpeltonen/sbt-idea plugin 37 | .idea_modules/ 38 | 39 | # JIRA plugin 40 | atlassian-ide-plugin.xml 41 | 42 | # Crashlytics plugin (for Android Studio and IntelliJ) 43 | com_crashlytics_export_strings.xml 44 | crashlytics.properties 45 | crashlytics-build.properties 46 | fabric.properties 47 | ### Java template 48 | *.class 49 | 50 | # Mobile Tools for Java (J2ME) 51 | .mtj.tmp/ 52 | 53 | # Package Files # 54 | *.jar 55 | *.war 56 | *.ear 57 | 58 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 59 | hs_err_pid* 60 | ### OCaml template 61 | *.annot 62 | *.cmo 63 | *.cma 64 | *.cmi 65 | *.a 66 | *.o 67 | *.cmx 68 | *.cmxs 69 | *.cmxa 70 | 71 | # ocamlbuild working directory 72 | _build/ 73 | 74 | # ocamlbuild targets 75 | *.byte 76 | *.native 77 | 78 | # oasis generated files 79 | setup.data 80 | setup.log 81 | ### OSX template 82 | *.DS_Store 83 | .AppleDouble 84 | .LSOverride 85 | 86 | # Icon must end with two \r 87 | Icon 88 | 89 | # Thumbnails 90 | ._* 91 | 92 | # Files that might appear in the root of a volume 93 | .DocumentRevisions-V100 94 | .fseventsd 95 | .Spotlight-V100 96 | .TemporaryItems 97 | .Trashes 98 | .VolumeIcon.icns 99 | .com.apple.timemachine.donotpresent 100 | 101 | # Directories potentially created on remote AFP share 102 | .AppleDB 103 | .AppleDesktop 104 | Network Trash Folder 105 | Temporary Items 106 | .apdisk 107 | *.log 108 | Main 109 | Main.hi 110 | 111 | node_modules 112 | venv 113 | 114 | src/go/go 115 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # if you want to use analyze-ocaml-instrumented, set this to a source 2 | # checkout of OCaml 4.03 or later 3 | OCAML_SOURCES=~/Prog/ocaml/github-trunk 4 | 5 | all: 6 | @echo "see Makefile for targets" 7 | 8 | # compile Haskell program 9 | haskell: Main.hs 10 | ghc -O2 -optc-O3 Main.hs 11 | 12 | clean:: 13 | rm -f Main.o Main.hi 14 | 15 | # run Haskell program and report times 16 | run-haskell: haskell 17 | ./Main +RTS -s 2> haskell.log 18 | cat haskell.log 19 | 20 | analyze-haskell: 21 | @echo "Worst old-generation pause:" 22 | @cat haskell.log | grep "Gen 1" | sed "s/ /\n/g" | tail -n 1 23 | 24 | # compile OCaml program 25 | ocaml: main.ml 26 | ocamlbuild -use-ocamlfind main.native 27 | 28 | clean:: 29 | ocamlbuild -clean 30 | 31 | # run OCaml program; only reports the last time 32 | run-ocaml: ocaml 33 | ./main.native 34 | 35 | # you need to "raco pkg install gcstats" first 36 | run-racket: 37 | PLT_INCREMENTAL_GC=1 racket -l gcstats -t main.rkt | tee racket.log 38 | 39 | analyze-racket: 40 | @grep "Max pause time" racket.log 41 | 42 | # run Racket program with debug instrumentation 43 | run-racket-instrumented: main.rkt 44 | PLTSTDERR=debug@GC PLT_INCREMENTAL_GC=1 racket main.rkt 2> racket.log 45 | 46 | # collect histogram from debug instrumentation, 47 | # to be used *after* run-racket 48 | analyze-racket-instrumented: 49 | cat racket.log | grep -v total | cut -d' ' -f7 | sort -n | uniq --count 50 | 51 | # these will only work if OCaml has been built with --with-instrumented-runtime 52 | ocaml-instrumented: main.ml 53 | ocamlbuild -use-ocamlfind -tag "runtime_variant(i)" main.native 54 | 55 | run-ocaml-instrumented: ocaml-instrumented 56 | OCAML_INSTR_FILE="ocaml.log" ./main.native 57 | 58 | analyze-ocaml-instrumented: 59 | $(OCAML_SOURCES)/tools/ocaml-instr-report ocaml.log | grep "dispatch:" -A13 60 | 61 | java: Main.java 62 | javac Main.java 63 | 64 | clean:: 65 | rm -f Main.class 66 | 67 | run-java: java 68 | java -verbosegc -cp . Main 69 | 70 | run-java-g1: java 71 | java -XX:+UseG1GC -verbosegc -cp . Main 72 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # GC latency experiment 2 | 3 | [My blog post][my-blog-post] about the results. [Original blog 4 | post][original-blog-post] that has the original experiment and code. 5 | 6 | Since writing the blog post I've run the benchmarks with newer versions of 7 | languages and added a Go benchmark. 8 | 9 | ## Run benchmarks 10 | 11 | ``` 12 | ./build-all.sh 13 | pip install -r requirements.txt 14 | python benchmark.py 15 | ``` 16 | 17 | ## Results 18 | 19 | The table and plots show the length of pauses caused by garbage collection. All 20 | times are in milliseconds except where noted. The results were collected by 21 | running all the tests 10 times in randomized order. 22 | 23 | All tests run with MacBook Pro (Retina, 15-inch, Late 2013) and macOS 10.12.6. 24 | 25 | ### Versions tested 26 | 27 | * Java 9.0.1 28 | * Scala 2.12.4 (running on Java 9.0.1) 29 | * Node.js 9.4.0 (ImmutableJS 3.8.2) 30 | * Haskell GHC 8.0.2 31 | * Go 1.9.2 32 | * Python 3.6.4 33 | * Swift 4.0.3 34 | 35 | ### Garbage collection pause times (milliseconds) 36 | 37 | | | Java | Scala | Node | Node Imm | Haskell | Go | 38 | | --------------------------- | ----: | ----: | ----: | -------: | ------: | ----: | 39 | | **Minimum** | 0.4 | 0.1 | 0.7 | 0.5 | 1.0 | 0.2 | 40 | | **Median** | 14.8 | 2.9 | 33.6 | 5.7 | 23.0 | 4.3 | 41 | | **Average** | 14.0 | 5.0 | 30.6 | 6.0 | 21.6 | 8.8 | 42 | | **Maximum** | 38.2 | 23.8 | 143.2 | 18.8 | 67.0 | 104.1 | 43 | | **Avg total pause per run** | 335.7 | 371.3 | 977.9 | 1252.2 | 239.7 | 88.4 | 44 | | **Avg pauses per run** | 24 | 73.8 | 32 | 207 | 11.1 | 10 | 45 | 46 | ### Run time (seconds) 47 | 48 | | | Java | Scala | Node | Node Imm | Haskell | Go | Python | Swift | 49 | | ----------- | ---: | ----: | ---: | -------: | ------: | ---: | -----: | ----: | 50 | | **Average** | 0.96 | 7.02 | 2.73 | 5.26 | 1.31 | 1.33 | 15.02 | 6.29 | 51 | 52 | ### Box plot of garbage collection pause times 53 | 54 | ![Box plot](gc-latencies.svg) 55 | 56 | ### Box plot zoomed in without extreme outliers 57 | 58 | ![Box plot zoomed in](gc-latencies-zoomed-in.svg) 59 | 60 | [my-blog-post]: https://blog.hilzu.moe/2016/06/26/studying-gc-latencies/ 61 | [original-blog-post]: http://prl.ccs.neu.edu/blog/2016/05/24/measuring-gc-latencies-in-haskell-ocaml-racket/ 62 | -------------------------------------------------------------------------------- /benchmark.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from subprocess import check_output, STDOUT 4 | from time import time 5 | from random import shuffle 6 | import matplotlib.pyplot as plt 7 | import re 8 | 9 | results = {} 10 | iterations = 10 11 | 12 | 13 | def benchmark(name, args, output_parser): 14 | start = time() 15 | output = check_output(args, universal_newlines=True, stderr=STDOUT) 16 | end = time() 17 | r = output_parser(output) 18 | if name not in results: 19 | results[name] = {"gc_times": [], "clock_times": []} 20 | results[name]["gc_times"].extend(r) 21 | results[name]["clock_times"].append(end - start) 22 | 23 | 24 | def parse_java8_gc_output(output): 25 | times = [] 26 | for line in output.split("\n"): 27 | if not line: 28 | continue 29 | if line.startswith("[GC concurrent"): 30 | continue 31 | m = re.search(r"(\d+[,.]\d+) secs]$", line) 32 | if not m: 33 | print("No match from line:", line) 34 | continue 35 | times.append(float(m.group(1).replace(",", ".")) * 1000) 36 | return times 37 | 38 | 39 | def parse_java9_gc_output(output): 40 | times = [] 41 | for line in output.split("\n"): 42 | if not line: 43 | continue 44 | m = re.search(r"GC\(\d+\) Pause.*?(\d+\.\d+)ms$", line.strip()) 45 | if not m: 46 | if "[gc]" in line: continue 47 | print("No match from line:", line) 48 | continue 49 | times.append(float(m.group(1))) 50 | return times 51 | 52 | 53 | def parse_v8_gc_output(output): 54 | times = [] 55 | for line in output.split("\n"): 56 | if not line: 57 | continue 58 | m = re.search(r", (\d+\.\d+) / 0.0 ms", line) 59 | if not m: 60 | print("No match from line:", line) 61 | continue 62 | times.append(float(m.group(1))) 63 | return times 64 | 65 | 66 | def parse_ghc_gc_output(output): 67 | times = [] 68 | for line in output.split("\n"): 69 | if not line: 70 | continue 71 | try: 72 | elems = line.split() 73 | float(elems[0]) 74 | float(elems[1]) 75 | m = elems[4] 76 | pause_time = float(m) 77 | except (IndexError, ValueError): 78 | print("No match from line:", line) 79 | continue 80 | if pause_time == 0: 81 | continue 82 | times.append(pause_time * 1000) 83 | return times 84 | 85 | 86 | def parse_go_gc_output(output): 87 | times = [] 88 | for line in output.split("\n"): 89 | line = line.strip() 90 | if not line.startswith("gc "): 91 | print("No match from line:", line) 92 | continue 93 | m = re.search(r"([0-9+.]+) ms clock", line) 94 | if not m: 95 | print("No match from line:", line) 96 | continue 97 | times.append(sum([float(x) for x in m.group(1).split("+")])) 98 | return times 99 | 100 | 101 | def benchmark_java(): 102 | benchmark("Java", ["java", "-verbosegc", "-cp", "src/java/", "-Xmx1G", "Main"], parse_java9_gc_output) 103 | 104 | 105 | def benchmark_java_g1(): 106 | benchmark( 107 | "Java G1", 108 | ["java", "-XX:+UseG1GC", "-verbosegc", "-cp", "src/java", "-Xmx1G", "-XX:MaxGCPauseMillis=50", "Main"], 109 | parse_java_gc_output, 110 | ) 111 | 112 | 113 | def benchmark_node(): 114 | benchmark("Node", ["node", "--trace-gc", "src/node/main.js"], parse_v8_gc_output) 115 | 116 | 117 | def benchmark_node_immutable(): 118 | benchmark("Node imm", ["node", "--trace-gc", "src/node/main-immutable.js"], parse_v8_gc_output) 119 | 120 | 121 | def benchmark_python(): 122 | benchmark("Python 3", ["python3", "src/python/main.py"], lambda x: []) 123 | 124 | 125 | def benchmark_pypy(): 126 | benchmark("PyPy", ["pypy", "src/python/main.py"], lambda x: []) 127 | 128 | def benchmark_swift(): 129 | benchmark("Swift", ["./src/swift/Main"], lambda x: []) 130 | 131 | def benchmark_scala(): 132 | benchmark("Scala", ["scala", "-cp", "src/scala", "-J-Xmx1G", "-J-verbosegc", "Main"], parse_java9_gc_output) 133 | 134 | 135 | def benchmark_haskell(): 136 | benchmark("Haskell", ["./src/haskell/Main", "+RTS", "-S"], parse_ghc_gc_output) 137 | 138 | 139 | def benchmark_go(): 140 | benchmark("Go", ["env", "GODEBUG=gctrace=1", "./src/go/go"], parse_go_gc_output) 141 | 142 | 143 | def avg(ls): 144 | return sum(ls) / len(ls) 145 | 146 | 147 | def median(ls): 148 | ls = sorted(ls) 149 | if len(ls) % 2 == 1: 150 | return ls[len(ls) // 2] 151 | else: 152 | return avg([ 153 | ls[len(ls) // 2 - 1], 154 | ls[len(ls) // 2], 155 | ]) 156 | 157 | 158 | def calculate_stats(times): 159 | if not times: 160 | return None 161 | fmt = "{0:.1f}" 162 | return { 163 | "average": fmt.format(avg(times)), 164 | "minimum": fmt.format(min(times)), 165 | "maximum": fmt.format(max(times)), 166 | "median": fmt.format(median(times)), 167 | "total": fmt.format(sum(times) / iterations), 168 | "amount": len(times) / iterations, 169 | } 170 | 171 | 172 | if __name__ == "__main__": 173 | benchmarks = [ 174 | benchmark_java, benchmark_node, benchmark_node_immutable, benchmark_python, 175 | benchmark_scala, benchmark_haskell, benchmark_swift, benchmark_go 176 | ] 177 | for i in range(iterations): 178 | shuffle(benchmarks) 179 | for b in benchmarks: 180 | b() 181 | print("\nRESULTS\n") 182 | data = [] 183 | labels = [] 184 | for name, res in results.items(): 185 | print(name) 186 | print("Average clock time:", "{0:.2f}".format(avg(res["clock_times"])), "s") 187 | print("GC pause times:", calculate_stats(res["gc_times"])) 188 | print("---") 189 | if res["gc_times"]: 190 | data.append(res["gc_times"]) 191 | labels.append(name) 192 | plt.boxplot(data, labels=labels, showmeans=True) 193 | plt.show() 194 | -------------------------------------------------------------------------------- /build-all.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | cd src/java/ 6 | javac -version 7 | javac Main.java 8 | 9 | cd ../scala 10 | scalac -version 11 | scalac Main.scala 12 | 13 | cd ../node 14 | echo -n "Node " 15 | node --version 16 | yarn install -s 17 | 18 | cd ../haskell 19 | stack ghc -- --version 20 | stack ghc -- -O2 -optc-O3 Main.hs 21 | 22 | cd ../swift 23 | swiftc --version 24 | swiftc -O -Xcc -O3 Main.swift 25 | 26 | cd ../go 27 | go version 28 | go build 29 | -------------------------------------------------------------------------------- /clean.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | rm -rf src/haskell/Main src/haskell/*.hi src/haskell/*.o \ 6 | src/java/*.class \ 7 | src/node/node_modules \ 8 | src/python/*.pyc \ 9 | src/scala/*.class \ 10 | src/swift/Main \ 11 | src/go/go 12 | -------------------------------------------------------------------------------- /gc-latencies-zoomed-in.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gc-latencies.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | matplotlib==2.1.0 2 | -------------------------------------------------------------------------------- /src/go/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | const windowSize int = 200000 4 | const msgCount int = 1000000 5 | const msgSize int = 1024 6 | var m map[int][msgSize]byte = make(map[int][msgSize]byte) 7 | 8 | func fill(bs [msgSize]byte, b byte) { 9 | for i, _ := range bs { 10 | bs[i] = b 11 | } 12 | } 13 | 14 | func createMessage(n int) [msgSize]byte { 15 | var msg [msgSize]byte 16 | fill(msg, byte(n)) 17 | return msg 18 | } 19 | 20 | func pushMessage(m map[int][msgSize]byte, id int) { 21 | var lowId int = windowSize - id 22 | m[id] = createMessage(id) 23 | if (lowId >= 0) { 24 | delete(m, lowId) 25 | } 26 | } 27 | 28 | func main() { 29 | for i := 0; i < msgCount; i++ { 30 | pushMessage(m, i) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/haskell/Main.hs: -------------------------------------------------------------------------------- 1 | module Main (main) where 2 | 3 | import qualified Control.Exception as Exception 4 | import qualified Control.Monad as Monad 5 | import qualified Data.ByteString as ByteString 6 | import qualified Data.Map.Strict as Map 7 | 8 | type Msg = ByteString.ByteString 9 | 10 | type Chan = Map.Map Int Msg 11 | 12 | windowSize = 200000 13 | msgCount = 1000000 14 | 15 | message :: Int -> Msg 16 | message n = ByteString.replicate 1024 (fromIntegral n) 17 | 18 | pushMsg :: Chan -> Int -> IO Chan 19 | pushMsg chan highId = 20 | Exception.evaluate $ 21 | let lowId = highId - windowSize in 22 | let inserted = Map.insert highId (message highId) chan in 23 | if lowId < 0 then inserted 24 | else Map.delete lowId inserted 25 | 26 | main :: IO () 27 | main = Monad.foldM_ pushMsg Map.empty [0..msgCount] 28 | 29 | -------------------------------------------------------------------------------- /src/java/Main.java: -------------------------------------------------------------------------------- 1 | import java.util.Arrays; 2 | import java.util.HashMap; 3 | 4 | public class Main { 5 | 6 | private static final int windowSize = 200_000; 7 | private static final int msgCount = 1_000_000; 8 | private static final int msgSize = 1024; 9 | 10 | private static byte[] createMessage(final int n) { 11 | final byte[] msg = new byte[msgSize]; 12 | Arrays.fill(msg, (byte) n); 13 | return msg; 14 | } 15 | 16 | private static void pushMessage(final HashMap map, final int id) { 17 | final int lowId = id - windowSize; 18 | map.put(id, createMessage(id)); 19 | if (lowId >= 0) { 20 | map.remove(lowId); 21 | } 22 | } 23 | 24 | public static void main(String[] args) { 25 | final HashMap map = new HashMap<>(); 26 | for (int i = 0; i < msgCount; i++) { 27 | pushMessage(map, i); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/node/main-immutable.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | const Immutable = require("immutable") 4 | 5 | const windowSize = 200000 6 | const msgCount = 1000000 7 | const msgSize = 1024 8 | 9 | const createMessage = n => Buffer.alloc(msgSize, n % 256) 10 | 11 | const pushMessage = (map, id) => { 12 | const lowId = id - windowSize 13 | const inserted = map.set(id, createMessage(id)) 14 | return lowId >= 0 ? inserted.delete(lowId) : inserted 15 | } 16 | 17 | const map = new Immutable.Map() 18 | 19 | new Immutable.Range(0, msgCount).reduce((map, i) => pushMessage(map, i), map) 20 | -------------------------------------------------------------------------------- /src/node/main.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | const windowSize = 200000 4 | const msgCount = 1000000 5 | const msgSize = 1024 6 | 7 | const createMessage = n => Buffer.alloc(msgSize, n % 256) 8 | 9 | const pushMessage = (map, id) => { 10 | const lowId = id - windowSize 11 | map.set(id, createMessage(id)) 12 | if (lowId >= 0) { 13 | map.delete(lowId) 14 | } 15 | } 16 | 17 | const map = new Map() 18 | for (let i = 0; i < msgCount; i++) { 19 | pushMessage(map, i) 20 | } 21 | -------------------------------------------------------------------------------- /src/node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gc-latency-experiment", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "main.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/Hilzu/gc-latency-experiment.git" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/Hilzu/gc-latency-experiment/issues" 17 | }, 18 | "homepage": "https://github.com/Hilzu/gc-latency-experiment#readme", 19 | "dependencies": { 20 | "immutable": "^3.8.2" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/node/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | immutable@^3.8.2: 6 | version "3.8.2" 7 | resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3" 8 | -------------------------------------------------------------------------------- /src/ocaml/_tags: -------------------------------------------------------------------------------- 1 | : precious 2 | true: package(batteries) 3 | -------------------------------------------------------------------------------- /src/ocaml/main.ml: -------------------------------------------------------------------------------- 1 | open Batteries 2 | 3 | module IMap = Map.Make(Int) 4 | 5 | type msg = string 6 | 7 | let message n = String.make 1024 (Char.chr (n mod 256)) 8 | 9 | let worst = ref 0. 10 | 11 | let window_size = 200_000 12 | let msg_count = 1_000_000 13 | 14 | let time f = 15 | let before = Unix.gettimeofday () in 16 | let result = f () in 17 | let after = Unix.gettimeofday () in 18 | worst := max !worst (after -. before); 19 | result 20 | 21 | let push_msg chan high_id = time @@ fun () -> 22 | let low_id = high_id - window_size in 23 | let inserted = IMap.add high_id (message high_id) chan in 24 | if low_id < 0 then inserted 25 | else IMap.remove low_id inserted 26 | 27 | let () = 28 | begin 29 | Seq.init 1_000_000 (fun i -> i) 30 | |> Seq.fold_left push_msg IMap.empty 31 | |> ignore 32 | end; 33 | Printf.printf "Worst pause: %.2E\n" !worst 34 | -------------------------------------------------------------------------------- /src/ocaml/parse_ocaml_log.ml: -------------------------------------------------------------------------------- 1 | (* This is a quick&dirty script to parse a log file produced by the 2 | OCaml instrumented runtime, version 4.03.0, and play with it from 3 | the toplevel using naive statistical functions. 4 | 5 | Build with 6 | ocamlbuild -use-ocamlfind -package batteries read_log.byte 7 | *) 8 | 9 | (* The format of the produced log files is currently undocumented, 10 | I just looked at it and guessed that the two numbers in most lines 11 | are the starting time and ending time of the recorded event (in 12 | nanoseconds). 13 | 14 | In practice not all lines correspond to time intervals in this way, 15 | some seem record integer variables instead. They will put 16 | a (usually small) integer in place of the second number. I'm not 17 | sure what in the format indicates which kind of data a given line 18 | is, so I use the dumb heuristic that (start <= stop) must hold for 19 | time intervals, and discard all lines not matching this condition. 20 | 21 | There is more knowledge in the file formats embedded in the tools 22 | included in the OCaml distribution, namely 23 | tools/ocaml-instr-report 24 | tools/ocaml-instr-graph 25 | 26 | If you wanted to write your own script or extend this one into 27 | a reusable library, you should go read the code of these tools to 28 | learn more about the log format. 29 | *) 30 | 31 | let read_log_line line = 32 | Scanf.sscanf line "@@ %19d %19d %s@\n" 33 | (fun start stop name -> 34 | if start > stop then None 35 | else Some (stop - start, name)) 36 | 37 | let events_of_file file = 38 | let ic = open_in file in 39 | (* ignore header file *) ignore (input_line ic); 40 | let events = 41 | let read_event () = read_log_line (input_line ic) in 42 | let (evs, _exn) = BatList.unfold_exc read_event in 43 | BatList.filter_map (fun ev_option -> ev_option) evs in 44 | close_in ic; 45 | let cmp (t1, _) (t2, _) = compare t1 (t2 : int) in 46 | List.sort cmp events |> Array.of_list 47 | 48 | let dispatch events = 49 | BatArray.filter (fun (_time, name) -> name = "dispatch") events 50 | 51 | let representatives n times = 52 | let len = Array.length times in 53 | let repr = Array.init n (fun i -> times.(i * len / n)) in 54 | (times.(0), repr, times.(len - 1)) 55 | 56 | let histogram n times = 57 | let len = Array.length times in 58 | let low, high = fst times.(0), fst times.(len - 1) in 59 | let bins = Array.make n [] in 60 | let bin ((time, _) as ev) = 61 | let i = (n - 1) * (time - low) / (high - low) in 62 | bins.(i) <- ev :: bins.(i); 63 | in 64 | Array.iter bin times; 65 | Array.map List.rev bins 66 | 67 | type display_bin = { count: int; min: int; max: int } 68 | let display_histogram histo = 69 | let display bin = 70 | if bin = [] then None 71 | else begin 72 | let times = List.map fst bin in 73 | let count = List.length times in 74 | let min, max = BatList.min_max times in 75 | Some { count; min; max } 76 | end in 77 | Array.map display histo 78 | -------------------------------------------------------------------------------- /src/python/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hilzu/gc-latency-experiment/ea4e8c9ebb1304b4da35d0dc33f08b59acf530d5/src/python/__init__.py -------------------------------------------------------------------------------- /src/python/main.py: -------------------------------------------------------------------------------- 1 | import platform 2 | 3 | if platform.python_implementation() == "CPython": 4 | import gc 5 | gc.set_debug(gc.DEBUG_STATS) 6 | 7 | windowSize = 200000 8 | msgCount = 1000000 9 | msgSize = 1024 10 | 11 | def create_message(n): 12 | return bytearray([n % 256] * msgSize) 13 | 14 | def push_message(dict_, id): 15 | low_id = id - windowSize 16 | dict_[id] = create_message(id) 17 | if low_id >= 0: 18 | del dict_[low_id] 19 | 20 | if __name__ == "__main__": 21 | dict_ = {} 22 | for i in range(msgCount): 23 | push_message(dict_, i) 24 | -------------------------------------------------------------------------------- /src/racket/main.rkt: -------------------------------------------------------------------------------- 1 | #lang racket/base 2 | 3 | (require racket/match) 4 | 5 | (define window-size 200000) 6 | (define msg-count 2000000) 7 | 8 | ; the incremental GC seems to have a hard time adapting its dynamic 9 | ; parameters to the end of the ramp-up period filling the chan. We can 10 | ; help by explicitly calling for GC collections around the end of this 11 | ; ramp-up period, but this seems to be rather sensitive to the 12 | ; specific parameters used (the limits of the ramp-up zone, and the 13 | ; frequency of collections). 14 | ; 15 | ; On Racket trunk, enabling this flag reduces the worst pause from 16 | ; 120ms to 40ms. 17 | (define gc-during-rampup #f) 18 | 19 | (define (maybe-gc i) 20 | (when (and gc-during-rampup 21 | (i . > . (window-size . / . 2)) 22 | (i . < . (window-size . * . 2)) 23 | (zero? (modulo i 50))) 24 | (collect-garbage 'incremental) 25 | (collect-garbage 'minor))) 26 | 27 | (define (message n) (make-bytes 1024 (modulo n 256))) 28 | 29 | (define (push-msg chan id-high) 30 | (define id-low (id-high . - . window-size)) 31 | (define inserted (hash-set chan id-high (message id-high))) 32 | (if (id-low . < . 0) inserted 33 | (hash-remove inserted id-low))) 34 | 35 | (define _ 36 | (for/fold 37 | ([chan (make-immutable-hash)]) 38 | ([i (in-range msg-count)]) 39 | (maybe-gc i) 40 | (push-msg chan i))) 41 | -------------------------------------------------------------------------------- /src/scala/Main.scala: -------------------------------------------------------------------------------- 1 | object Main { 2 | val windowSize = 200000 3 | val msgCount = 1000000 4 | val msgSize = 1024 5 | 6 | def createMessage(n: Int): Array[Byte] = { 7 | Array.fill(msgSize){ n.toByte } 8 | } 9 | 10 | def pushMessage(map: Map[Int, Array[Byte]], id: Int): Map[Int, Array[Byte]] = { 11 | val lowId = id - windowSize 12 | val inserted = map + (id -> createMessage(id)) 13 | if (lowId >= 0) inserted - lowId else inserted 14 | } 15 | 16 | def main(args: Array[String]): Unit = { 17 | val map = Map[Int, Array[Byte]]() 18 | (0 until msgCount).foldLeft(map) { (m, i) => pushMessage(m, i) } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/swift/Main.swift: -------------------------------------------------------------------------------- 1 | let windowSize = 200_000 2 | let msgCount = 10_000_000 3 | let msgSize = 1024 4 | 5 | func createMessage(_ n: Int) -> Array { 6 | return Array(repeating: UInt8(n % 256), count: msgSize) 7 | } 8 | 9 | func pushMessage (map: inout [Int: Array], id: Int) { 10 | let lowId = id - windowSize 11 | map[id] = createMessage(id) 12 | if lowId >= 0 { 13 | map.removeValue(forKey: lowId) 14 | } 15 | } 16 | 17 | var map = [Int: Array]() 18 | for i in 0...(msgCount - 1) { 19 | pushMessage(map: &map, id: i) 20 | } 21 | --------------------------------------------------------------------------------