-
Notifications
You must be signed in to change notification settings - Fork 3.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Inefficient CPU utilization when compiling dependencies #14200
Comments
This is tricky because compiling Elixir code means running Elixir code, and that code may rely on global values, the most obvious one being the current working directory. Therefore, the only way we could optimize this is by starting multiple What we could do is a graph analysis and see if the graph has reasonably distinct branches, and emit a separate |
Could there be a global cache somehow (like a mix supervisor or something) that could cache per application to avoid over compiling?
Using this as an example, we could check the cache for a compiled plug, and return its path if it already exists, otherwise, compile plug? |
I'd say that's a separate problem. One is caching and the other is optimizing the (uncached) build itself. When it comes to caching, keep in mind that compile time configuration and optional dependencies may lead to different builds for a single dep, and the issue is that a bug in caching can lead to very subtle, hard to debug, issues. For caching itself, I'd prefer to explore first caching within the same build but different results, as in #12520, because I think our compilation graph is robust enough to deal with that and recompile stale parts. |
The scope of this issue is for the uncached build :)
It is really unfortunate that such a highly concurrent language has a concept of a global working directory, though I guess given that erlang has it as I have no issue with spawning new mix processes to accomplish parallel compilation. I do wonder how expensive it is to redundantly reload compiled beam files across multiple VMs? I have to imagine that for the small, handful-of-files types of projects that seem to bottleneck my compilation today, it would be clearly worth it to do this. EDIT: also, maybe independent mix processes could avoid the overhead of code reloading by making compilation use a distributed erlang setup, and having the compiled code reside on the node that compiled it? |
Most things which are system related, OS variables, current working directory, or even the file system itself, are global shared resources. Of course, we could provide some reasonable "shielding" but at some point the rubber has to hit the road.
The issue may not be reload per se but introducing synchronization points. If we can split the dep tree into subtrees without dependencies, then everyone's life will be easier. :) When I have some time, I will provide a script that computes these subtrees, so you and other people could experiment with this. |
I made an unoptimized proof of concept demo that compiles dependencies quite a lot faster using Elixir v1.17 (note that a lock was added recently which would probably prevent this from working as intended on v1.18, but I did not test that)
# iex -S mix
# then in a different terminal: rm -rf _build
iex(1)> Code.require_file("dep_solver.ex")
# ...
iex(2)> :timer.tc(fn -> ConcurrentDependencyProcessor.process_all_concurrently() end)
# ...
{16039126,
[:ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok,
:ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok, :ok,
:ok, :ok]} so a 1.77x speedup, and the CPU is fully utilized until the very end. |
Elixir and Erlang/OTP versions
Erlang/OTP 27 [erts-15.2] [source] [64-bit] [smp:32:32] [ds:32:32:10] [async-threads:1] [jit:ns]
Elixir 1.18.1 (compiled with Erlang/OTP 27)
Operating system
Linux
Current behavior
I have an application which has a lot of its own files and a fair number of dependencies. When running
mix deps.compile
for the first time (so no_build
directory exists at this point), mix spends a lot of time compiling applications, one after the other, in a serial fashion.The problem with this approach is that I have a 16-core, 32-logical-core processor, and
System.schedulers_online()
returns 32, so mix should really always be trying to schedule 32 compilation units between applications with satisfied dependencies, which it does not appear to be doing.Since most dependency applications are just a handful of files, this results in just a few cores being used most of the time. The logging and utilization suggests that all of the files within each application get compiled in parallel; then the result of all compilations is awaited before generating the next application.
An example of a deps list in a new mix project that doesn't make the best use of the CPU during compilation follows:
Example
-> 3.83x speedup over serial compilation
Expected behavior
My application contains 640+ elixir compilation units. After cleaning and running
mix compile
, mix will max out the CPU immediately and consistently schedule work for all 32 logical cores until there are no new compilation tasks to complete.Example:
-> 7.19x speedup over serial compilation
This would also be the desired behavior when compiling application dependencies.
The text was updated successfully, but these errors were encountered: