├── .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 | 
55 |
56 | ### Box plot zoomed in without extreme outliers
57 |
58 | 
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 |
--------------------------------------------------------------------------------