Why does Julia use so much memory?

Julia seems to have a pretty high memory footprint. For example, compared to Lisp, which has similar features (dynamic types, homoiconic, incremental compling etc), Julia is pretty bad [1]. Is it just because Julia is still young, or are there some coding traps that blow up your memory usage?

[1] Julia vs Lisp SBCL - Which programs are fastest?

It’s probably because those benchmarks include JIT compilation. Do the LISP benchmarks include the cost and memory use of compilation, or does that happen in a separate step first?

Well-written Julia code can be very memory efficient, but the JIT compilation step has a significant memory footprint.

3 Likes

I can’t tell for sure when SBCL does compilation, but it does seem to compile directly to native code [1]. :man_shrugging:

[1] SBCL 2.2.8 User Manual

They include the make process in the benchmarksgame underneath the code. The compilation of SBCL does not seem to be included in the benchmark values.

Right, so the SBCL code is precompiled. That’s good to know. It looks like the Julia runtime is using about 150MB. When I start a julia repl, ps reports it is using about 178MB of memory (RES), so that seems about right.

E.g. LLVM, the dependency/backend of the compiler takes a lot of memory, and compiling, (it and the frontend) adds some startup cost, which can be avoided. Without that startup cost, Julia is the fastest language I checked, on everything (e.g. compared to C), for those benchmarks.

I.e. you can compile Julia code to static binaries, it’s just not a default. By default with PackageCompiler you can do that, and it will include LLVM still by default, but you can also get rid of it with some option in Julia 1.8-DEV, which seems to be around the corner, i.e. 1.8-beta1.

Lisp has many implementations. Lisp SBCL is just one of them, and it’s entirely unfair that the BenchmarkGame only allows what Julia does by default. It puts pressure on forking Julia to include the non-default options (or add them into mainline).

150MB was a pretty good guess:

$ /usr/bin/time -v julia-1.7 -e0
	Command being timed: "julia-1.7 -e0"
	User time (seconds): 0.28
	System time (seconds): 0.18
	Percent of CPU this job got: 112%
	Elapsed (wall clock) time (h:mm:ss or m:ss): 0:00.41
	Average shared text size (kbytes): 0
	Average unshared data size (kbytes): 0
	Average stack size (kbytes): 0
	Average total size (kbytes): 0
	Maximum resident set size (kbytes): 154064
	Average resident set size (kbytes): 0
	Major (requiring I/O) page faults: 0
	Minor (reclaiming a frame) page faults: 24013
	Voluntary context switches: 2
	Involuntary context switches: 6
	Swaps: 0
	File system inputs: 0
	File system outputs: 0
	Socket messages sent: 0
	Socket messages received: 0
	Signals delivered: 0
	Page size (bytes): 4096
	Exit status: 0
1 Like

I always thought that Julia loads pretty big linear algebra libraries, like openBLAS. You can try to build system image without those, than footprint should be better.

1 Like

You could try that, but while BLAS is big (in lines of assembly) code, I’m not sure it matters much. Loading a shared .so should be fast, and I don’t worry about relatively small BLAS at 31 MB. LLVM has the largest static code size, but you should worry about the time and actual dynamic memory size (BLAS wouldn’t be exercised) of LLVM:

$ ls -lrSh ~/julia-1.7.0/lib/julia/ |tail
-rwxr-xr-x 1 pharaldsson_sym pharaldsson_sym 1,5M nóv 30 18:59 libgit2.so.1.1.0
-rwxr-xr-x 1 pharaldsson_sym pharaldsson_sym 1,6M nóv 30 18:59 libllvmcalltest.so
-rwxr-xr-x 1 pharaldsson_sym pharaldsson_sym 2,3M nóv 30 18:59 libmpfr.so.6.1.0
-rwxr-xr-x 1 pharaldsson_sym pharaldsson_sym 2,9M nóv 30 18:59 libblastrampoline.so
-rwxr-xr-x 1 pharaldsson_sym pharaldsson_sym 8,8M nóv 30 18:59 libgfortran.so.5.0.0
-rwxr-xr-x 1 pharaldsson_sym pharaldsson_sym  18M nóv 30 18:59 libstdc++.so.6.0.29
-rwxr-xr-x 1 pharaldsson_sym pharaldsson_sym  31M nóv 30 18:59 libopenblas64_.0.3.13.so
-rwxr-xr-x 1 pharaldsson_sym pharaldsson_sym  41M nóv 30 19:13 libjulia-internal.so.1.7
-rwxr-xr-x 1 pharaldsson_sym pharaldsson_sym  76M nóv 30 19:13 libLLVM-12jl.so
-rwxr-xr-x 1 pharaldsson_sym pharaldsson_sym 191M nóv 30 19:12 sys.so

$ ls -lrSh ~/julia-1.8-DEV-e422590151/lib/julia/ |tail
-rwxr-xr-x 1 pharaldsson_sym pharaldsson_sym 9,9M feb 12 23:30 libjulia-internal.so.1.8
[..]
-rwxr-xr-x 1 pharaldsson_sym pharaldsson_sym  36M feb 12 22:46 libjulia-codegen.so.1.8
-rwxr-xr-x 1 pharaldsson_sym pharaldsson_sym  79M feb 12 22:45 libLLVM-13jl.so
-rwxr-xr-x 1 pharaldsson_sym pharaldsson_sym 210M feb 12 23:28 sys.so

I can’t tell for sure when SBCL does compilation, but it does seem to compile directly to native code

SBCL compiles file-by-file or file-by-file, before being added in to a live image. Hence the compilation is incremental. The SBCL compiler is very advanced, with a powerful debugger, and compiler notes to help improve code efficiency.

Yes, like most Lisps, SBCL does incremental compilation. However, for the benchmarks game their logs show the programs were precompiled. Cheaters!

2 Likes

Just for fun, here is the memory footprint of a few different languages.

$ /usr/bin/time --format "%M kB" kotlin-1.6.0 -e 0
203844 kB
$ /usr/bin/time --format "%M kB" julia-1.7 -e0
152796 kB
$ /usr/bin/time --format "%M kB" racket-7.2 -I typed/racket -e 0
147868 kB
$ /usr/bin/time --format "%M kB" racket-7.2 -e 0
67280 kB
$ /usr/bin/time --format "%M kB" nodejs-10.21.0 -e 0
34752 kB
$ /usr/bin/time --format "%M kB" racket-7.2 -I racket/base -e 0
28772 kB
$ /usr/bin/time --format "%M kB" sbcl-1.4.16 --noinform --eval 0 --quit
19148 kB
$ /usr/bin/time --format "%M kB" python-3.7.3 -c0
9248 kB
$ /usr/bin/time --format "%M kB" csi-4.13 -e 0
7628 kB
$ /usr/bin/time --format "%M kB" python-2.7.16 -c0
6576 kB
3 Likes

I checked for fun also perl, and as expected it has lowest usage (also fastest startup), of scripting languages:

[while I also intriguingly discovered, it as with all languages, I checked including Julia, I do not get a deterministic amount]

$ /usr/bin/time --format "%M kB" perl -e0
4680 kB
$ /usr/bin/time --format "%M kB" perl -e0
4556 kB

well it's lowest except for:

$ /usr/bin/time --format "%M kB" bash -c ""
3296 kB
$ /usr/bin/time --format "%M kB" dash -c ""
1612 kB

$ /usr/bin/time --format "%M kB" awk ""
1860 kB

Julia’s amount is getting larger, comparing my 1.8-DEV “(8 days old master)” to 1.7.0. Can someone check with a non-default sysimage, where LLVM (and/or BLAS) is stripped out? As I thought, not all of the sysimage (sys.so), and likely not LLVM, BLAS etc. are loaded, since julia’s memory usage is lower than even just sys.so.

1 Like

While a little higher at 168576 kB in Julia 1.8.1, an additional bonus to starting twice as fast:

$ /usr/bin/time --format "%M kB" julia -J BenchmarkingSysimage.so --startup-file=no --history-file=no -e0
95804 kB