Yet another question about Julia's high RAM usage

Summary of my current understanding: Julia has a relatively high memory footprint for an interactive language for various reasons: 1) an OpenBLAS implementation that preallocates buffers to accelerate matrix multiplication that cannot be removed from Base, 2) a JIT compiler that interpreted languages don’t use, 3) overheads for modules, types, docstrings, and all the good stuff we need for interactivity. This hasn’t changed much in the last few years, but the upcoming --trim option of juliac is bringing an era of non-interactive AOT compilation where we could potentially remove this overhead for contexts that call for it.

Eyeballing the Windows Task Manager, I checked what the numbers are currently (v1.11.6). In a fresh REPL session started from Windows Powershell:

julia> # typing goes from 157 to 161MB, typical over last few years

julia> using Example # typing creeps up to 166MB

julia> hello(string(domath(2))) # only 2 functions in Example
"Hello, 7"

julia> # 167MB here

julia> hello(domath(10)) # I know this will throw a MethodError
ERROR: MethodError: no method matching hello(::Int64)
...
julia> # jumped to 257MB

(@v1.11) pkg> st
...
julia> # jumped to 386MB

I was really surprised by the massive jumps of ~90MB and ~130MB for seemingly small things like throwing a MethodError or using the Pkg REPL mode to check my environment. This also means that the ~150-200MB estimate for loading the REPL is too low for estimating practical REPL usage even if we don’t use much more than what’s already evaluated. By comparison, Python v3.13.3’s REPL takes a little under 10MB to start, and I could import a dozen standard libraries (I’m assuming Python wrappers of C binaries) to just manage 15MB; throwing errors didn’t change the RAM usage. To get an idea of what varying amounts of RAM can do, the 5 PlayStation generations had 2+1MB, 32+4MB, 256+256MB, 8GB, and 16GB of system+video RAM or unified RAM.

Can anybody explain those huge jumps? I know the native code is different and isn’t comparable, but could Julia have a much larger overhead for managing the existence of modules, types, methods, etc?

EDIT: Suspecting platform dependence, so including my versioninfo here.

julia> versioninfo()
Julia Version 1.11.6
Commit 9615af0f26 (2025-07-09 12:58 UTC)
Build Info:
  Official https://julialang.org/ release
Platform Info:
  OS: Windows (x86_64-w64-mingw32)
  CPU: 8 × Intel(R) Core(TM) i7-1065G7 CPU @ 1.30GHz
  WORD_SIZE: 64
  LLVM: libLLVM-16.0.6 (ORCJIT, icelake-client)
Threads: 1 default, 0 interactive, 1 GC (on 8 virtual cores)
9 Likes

I wonder how much of this is allocations of strings during compilation of various error paths. Strings, due to being semantically immutable, don’t get garbage collected as far as I remember. They are also constant propagated, which can cause more allocations, even though the compiler doesn’t actually use that result.

Strings do get GC’d, but code and symbols do not.

There might be something to do about the large memory footprint, but probably not that much. The whole premise of Julia is that RAM is so cheap that we can just make every function generic, monomorphize everything and generate all the custom code on demand.

So yeah, there are all the things you write, but then Julia also makes it hard to make sure there aren’t 50 unnecessary specializations of a bunch of functions.

7 Likes

FYI, I was curious, but I don’t observe those memory jumps on my Windows machine using Julia 1.10.9 from the terminal (not VSCode) - everything stays around 50 MB from using Example to the MethodError.

2 Likes

That’s interesting, I tried 1.10.9 and more or less reproduced the first post with different numbers: 111, 112, 248, 307MB, the jumps being ~140MB and ~60MB instead. Could you elaborate with how you were checking the RAM usage and your versioninfo? Here’s mine for 1.10.9:

julia> versioninfo()
Julia Version 1.10.9
Commit 5595d20a28 (2025-03-10 12:51 UTC)
Build Info:
  Official https://julialang.org/ release
Platform Info:
  OS: Windows (x86_64-w64-mingw32)
  CPU: 8 × Intel(R) Core(TM) i7-1065G7 CPU @ 1.30GHz
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-15.0.7 (ORCJIT, icelake-client)
Threads: 1 default, 0 interactive, 1 GC (on 8 virtual cores)

That’s nothing. For me, just starting Julia raises memory to almost 1 GB. Middle process in the attached fig. The other two are due to two instances of VSC.
I only have Revise in my startup.

1 Like

Sorry, I was looking at the wrong entry in Windows Task Manager. Here are the correct numbers:

  • Startup: 148 MB
  • julia> using Example: 148 MB
  • julia> hello(string(domath(2))): 148 MB
  • hello(domath(10)): 285 MB

Julia Version 1.10.9
Commit 5595d20a28 (2025-03-10 12:51 UTC)
Build Info:
Official https://julialang.org/ release
Platform Info:
OS: Windows (x86_64-w64-mingw32)
CPU: 12 × 13th Gen Intel(R) Core™ i7-1365U
WORD_SIZE: 64
LIBM: libopenlibm
LLVM: libLLVM-15.0.7 (ORCJIT, goldmont)
Threads: 1 default, 0 interactive, 1 GC (on 12 virtual cores)

I get slightly different numbers on different startups and I think the main reason is that type inference is triggered differently depending on your environment and startup configuration. The increased memory consumption when the MethodError is hit also happens only the first time.

1 Like

Not sure if this could play a role, but playing around I found out that there was some “problem” in julia 1.11 and 1.10 of high memory consumption the first time something is printed

julia> @time println("Julia is great!")
Julia is great!
  0.005622 seconds (5.72 k allocations: 319.180 KiB, 98.96% compilation time)

julia> @time println("Julia is great!")
Julia is great!
  0.000043 seconds (5 allocations: 80 bytes)

julia> 

But it seems to be resolved in the 1.12 beta

julia> @time println("Julia is great!")
Julia is great!
  0.000048 seconds

julia> @time println("Julia is great!")
Julia is great!
  0.000035 seconds

That looks more like JIT compilation, and that just seems to suggest that 1.12 has println(::String) precompiled in the sysimage. It’s fairly typical that sysimages and packageimages only contain native code for a small fraction of useful call signatures; I say useful because “possible” is astronomically large for generic functions with very permissive argument annotations. I should’ve mentioned this in my post, but a baseline estimate for “practical REPL usage” is not a rigorous concept, just something to keep in mind when trying to use Julia on systems with <8GiB of RAM, because there isn’t a standard way to use Julia. Some people may never print or use Pkg.

@time doesn’t only measure persisting allocations, so it’s likely not all of that 319 KiB would persist. For comparison, @time Pkg.status() on v1.11.6 reports 173 MiB for me, much more than the persisting 90MB jump. Would be nice if I knew a tool to catch what is compiled and measure native code sizes for a lower bound.

On Linux, measured in total virtual memory (VIRT in top):

  1. merely starting up julia is 1.3 Gb,
  2. using Example takes it to nearly 2 Gb,
  3. the hello(domath(10)) error takes it to around 2363008 bytes.

But, from that point on, whatever I throw at it (eg running all the DynamicHMC.jl test suite), it does not change. RES climbs up to 360k, but that is still OK by my standards.

julia> versioninfo()
Julia Version 1.12.0-rc1
Commit 228edd6610b (2025-07-12 20:11 UTC)
Build Info:
  Official https://julialang.org release
Platform Info:
  OS: Linux (x86_64-linux-gnu)
  CPU: 8 × 11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz
  WORD_SIZE: 64
  LLVM: libLLVM-18.1.7 (ORCJIT, tigerlake)
  GC: Built with stock GC
Threads: 4 default, 1 interactive, 4 GC (on 8 virtual cores)
Environment:
  JULIA_NUM_THREADS = 4

This is somewhat of an eye-opener.

4 Likes

But VIRT is pretty much meaningless on linux – this only counts the sum of all ranges that have been mapped in the OS. RES is the important thing (which counts the sum of all ranges that have been mapped in the OS and that are currently backed by physical memory).

VIRT memory is too cheap to meter, while physical memory is the thing that you have to pay money for, and crawl under your desk to put a new stick in.

The situation is somewhat different on windows – there, good software needs to be stingy with virtual memory (but in exchange, the windows folk don’t get OOM-killer hell when reality catches up with overcommit – a different tradeoff that has its perks).

3 Likes

I’ll admit here that I have no idea if Windows Task manager is measuring virtual memory or physical memory usage. I’m guessing physical because the Julia process goes way down if I leave it alone and use other applications for a while, and when I interact with it again, it doesn’t go back to what I’d expect. EDIT: yep, the number I’m eyeballing is labelled physical memory.

At all? Even if the JIT-compiled code doesn’t take up much space, I’d still expect it to be visible.

That goes up: starts with about 364k, using Example pushes it up to 380k, it stays there for the working hello, then the error pushes it up to a whopping 480k (!). Errors are apparently costly :wink:

Then throwing the DynamicHMC.jl tests at it pushes RES up to 754k, all of which comes at the package loading stage. The actual test execution is not changing anything.