Code Monkey home page Code Monkey logo

nocc's Introduction

nocc — a distributed C++ compiler

nocc propagates a compiler invocation to a remote machine: nocc g++ 1.cpp calls g++ remotely, not locally.

nocc speeds up compilation of large C++ projects: when you have multiple remotes, tons of local jobs are parallelized between them.

But its most significant effort is greatly speeding up re-compilation across build agents in CI/CD and across developers working on the same project: they use shared remote caches. Once a cpp file has been compiled, the resulting obj is used by other agents without launching compilation, actually.

nocc easily integrates into any build system, since a build system should only prefix executing commands.


The reason why nocc was created

nocc was created at VK.com to speed up KPHP compilation. KPHP is a PHP compiler: it converts PHP sources to C++. VK.com codebase is huge, for how we have about 150 000 autogenerated cpp files.

Our goal was to greatly improve the performance of the "C++ → binary" step.

Since 2014, we used distcc.
In 2019, we patched distcc to support precompiled headers. That gave us 5x to performance.
In 2021, we decided to implement a distcc replacement. Finally, we got 2x – 9x over the patched version.


Installation and configuration

The easiest way is just to download ready binaries — proceed to the releases page and download the latest .tar.gz for your system: you'll have 3 binaries after extracting.

You can also compile nocc from sources, see the installation page.

For a test launch (to make sure that everything works), proceed to this section.

For a list of command-line arguments and environment variables, visit the configuration page.


How does nocc work

Consider the following file named 1.cpp:

#include "1.h"

int square(int a) { 
  return a * a; 
}

Having 1.h be just like

int square(int a);

When you run nocc g++ 1.cpp -o 1.o -c, the compilation is done remotely:

one file

What's actually happening here:

  • nocc parses the command-line invocation: input files, include dirs, cxx flags, etc.
  • for an input file (1.cpp), nocc finds all dependencies: it traverses all #include recursively (which results in just one file 1.h here)
  • nocc uploads files to a server and waits
  • nocc-server executes the same command-line (same cxx flags, but modified paths)
  • nocc-server pushes a compiled object file back
  • nocc saves 1.o — the same as if compiled locally

Besides an object file, nocc-server pushes exitCode/stdout/stderr of the C++ compiler: nocc process uses them as a self output.

In production, you have multiple compilation servers

Conceptually, you can think of a working scheme like this:

many files

Lots of nocc processes are launched simultaneously — much more than you could launch if you use g++ locally.

Every nocc invocation handles exactly one .cpp -> .o compilation, it's by design. It does remote compilation and dies — nocc is just a front-end layer between any build system and a real C++ compiler.

For every invocation, a remote server is chosen, all dependencies are detected, missing dependencies are uploaded, and the server streams back a ready obj file. This happens in parallel for all command lines.

Actually, to be more efficient, all connections are served via one background nocc-daemon:

daemon

nocc-daemon is written in Go, whereas nocc is a very lightweight C++ wrapper, the only aim of which is to pipe command-line to a daemon, wait for the response, and die.

So, a final working scheme is the following:

  1. The very first nocc invocation starts nocc-daemon: a daemon serves grpc connections and actually does all stuff for remote compilation.
  2. Every nocc invocation pipes a command-line (g++ ...) to a daemon via Unix socket, a daemon compiles it remotely and writes the resulting .o file, then nocc process dies.
  3. nocc jobs start and die: a build system executes and balances them.
  4. nocc-daemon dies in 15 seconds after nocc stops connecting (after the compilation process finishes).

For more info, consider the nocc architecture page.


nocc is also a remote src/obj cache

The main idea behind nocc is that the 2nd, the 3rd, the Nth runs are faster than the first. Even if you clean a build directory, even on another machine, even in a renamed folder.

That's because of remote caches.
nocc does not upload files if they have already been uploaded — that's the src cache.
nocc does not compile files if they have already been compiled — that's the obj cache.

second run

Such an approach dramatically decreases compilation times if your CI has different build machines or your builds start from a fresh copy. Moreover, git branch switching and merging is also a great target for remote caching.


nocc and CMake

When CMake generates a buildfile for your C++ project, you typically launch the build process with make or ninja. These build systems launch and balance processes and keep doing it until all C++ files are compiled.

Our goal is to tell CMake to launch nocc g++ instead of g++ (or any other C++ compiler). This can be done with -DCMAKE_CXX_COMPILER_LAUNCHER:

cmake -DCMAKE_CXX_COMPILER_LAUNCHER=/path/to/nocc ..

Then make building would look like this:

make

CMake sometimes invokes the C++ compiler with -MD/-MT flags to generate a dependency list. nocc supports them out of the box, depfiles are generated on a client-side.


nocc and ninja

Ninja is a build system, easily integrated to CMake instead of make.

nocc works with ninja, but there are 2 points to care about:

  1. Explicitly set -j {jobs} (typically, you don't do this with ninja, then it automatically spreads jobs across machine CPUs, but we need {jobs} to be a huge number).
  2. There is an upsetting defect that (whyever) ninja incrementally waits for a daemon to die. A workaround is to launch a daemon manually in advance. Read more about this problem.


nocc and KPHP

Originally, nocc was created to speed up compiling large KPHP projects, with lots of autogenerated C++ files. KPHP does not call make: it has a build system right inside itself.

To use nocc with KPHP, just set the KPHP_CXX=nocc g++ environment variable. Then nocc will be used for both C++ compilation and precompiled headers generation.


Precompiled headers support

nocc treats precompiled headers in a special way. When a client command to generate pch is executed,

nocc g++ -x c++-header -o all-headers.h.gch all-headers.h

then nocc emits all-headers.h.nocc-pch, whereas all-headers.h.gch is not produced at all. This is a text file containing all dependencies — compiled on a server-side into a real .gch/.pch.

Generating a .nocc-pch file is much faster than generating a real precompiled header, so it's acceptable to call it for every build — anyway, it will be compiled remotely only once.

Here you can read more about own precompiled headers.


nocc vs ccache

It's quite incorrect to compare nocc with ccache, as ccache is not intended to parallelize compilation on remotes. ccache can speed up compilation performed locally (especially useful when you switch git branches), but when it comes to compiling a huge number of C++ files from scratch, everything is still done locally.

nocc also greatly speeds up re-compilation when switching branches. But nocc does it in a completely different ideological way: using remote caches.


nocc vs distcc

Because nocc was targeted as a distcc replacement, a detailed analysis of their differences is written on the compare with distcc page.

That page includes an architecture overview, some info about patching distcc with pch support, and real build times from VK.com production.


What makes nocc so fast

nocc architecture is specially tuned to be as fast as possible for typical usage scenarios.

  • nocc-daemon keeps all connections alive, while nocc processes start and die during a build
  • to resolve all recursive #include, nocc does not invoke preprocessor: it uses its own parser instead
  • nocc-server has the src cache: once 1.h is uploaded by any client, no other clients need to upload this file again (unless changed)
  • nocc-server has the obj cache: once 1.cpp is compiled by any client, all other clients receive 1.o without compilation (if all dependencies and flags match)
  • for a file.cpp, one and the same server is chosen every time to make remote caches reusable
  • shared precompiled headers: once 1.gch compiled, no other build agents have to do it locally

Dig deeper into nocc architecture


FAQ

What are the conditions to make sure that a remote .o file would equal a local .o?

nocc assumes that all remotes have the C++ compiler of exactly the same version as local. That would ensure no difference, where exactly the compilation was launched if we have equal source files. Since linking is done locally, remotes are not required to have all libs needed for linking.

What if I #include <re2.h> but it doesn't exist on remote?

Everything would still work. When nocc traverses dependencies, it also finds all system headers recursively, their hash sums are sent to the remote along with the cpp file info. If some system includes are missing (or if they differ from local ones), they are also sent like regular files, saved to the /tmp folder representing client file structure, and discovered via special -isystem arguments added to the command-line.

How does nocc handle linking commands?

Linking is done locally. All commands that are unsupported or non-well-formed are done locally.

What happens if some servers are unavailable?

When nocc tries to compile 1.cpp remotely, but the server is unavailable, nocc falls back to local compilation. It does not try another server, it's intentionally.

Does nocc support clang?

Theoretically, there should be no difference, what compiler is being used: g++, or clang++, or /usr/bin/c++, etc. Even .pch files are supposed to work, as pch compilation is done remotely. Small tests for clang work well, but it hasn't been tested well in production, as we use only g++ in KPHP and VK.com for now.

What is the optimal job/server count?

The final number that we fixated at VK.com is "launch ~20 jobs for one server". For example, we have 32 compilation servers, and we launch ~600 jobs for C++ compilation. This works well both when files are compiled and when they are just taken from obj cache. Note, that if you use a large number of parallel jobs, you'd probably have to increase ulimit -n, as nocc-daemon reads lots of files and keeps all connections to nocc C++ wrappers simultaneously.

I get an error "compiling locally: rpc error: code = Unknown desc = file xxx.cpp was already uploaded, but now got another sha256 from client"

This error occurs in such a scenario: you compile a file, they quickly modify it, and launch compilation again — a previous nocc-daemon is still running, previous file structure is still mapped to servers. Then the compilation for such file is done locally. In reality, such an error never occurs, as big projects take some time for linking/finalization after compilation (a daemon dies in 15 seconds).

Why did you name this tool "nocc"?

We already have a PHP linter named noverify and an architecture validation tool nocolor. That's why "nocc" — just because I like such naming :)

nocc's People

Contributors

unserialize avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

nocc's Issues

wrong handling of current working directory in case of submake

In case of submake call nocc is not changing submake root working directory. This leads to wrong place of obj files generated with submake.

How to reproduce:
# Configure nocc env vars to access nocc cluster
git clone -b v0.3.27 https://github.com/OpenMathLib/OpenBLAS.git
DYNAMIC_ARCH=1 make CC="nocc ${CC}" CXX="nocc ${CXX}" libs

Using on-demand servers

Documentation says that the same remote server is always used for the same file, in order to maintain remote cache.

We have a set of local machines for building, but when they get saturated, we want to dynamically spin up AWS nodes to serve build requests - when the demand has waned for a bit, spin them down.

How can we use nocc if we dynamically spin AWS instances up and down on demand?

nocc client hangs on include files that ends with single-line commend containing #

Setup:

inc.h

No new line at the end of file! a - is the last symbol of the file.

// blabla # blabla

src.c

#include "inc.h"

Commands

nocc-server -log-filename nocc_server.log -log-verbosity 2
NOCC_GO_EXECUTABLE=nocc-daemon NOCC_SERVERS="127.0.0.1:43210" NOCC_LOG_FILENAME=nocc_client.log NOCC_LOG_VERBOSITY=2 nocc gcc -I. -o src.o -c src.c

Result

Client hangs (presumably stuck at analyzing includes).

Logs:

nocc_server.log

2024-04-19 13:11:50 INFO nocc-server started
2024-04-19 13:11:50 INFO env: listenAddr 0.0.0.0:43210 ; ulimit -n 4096 ; num cpu 8 ; version v1.2.1, rev 7510928, compiled at 2024-04-19 10:07:51 UTC
2024-04-19 13:11:57 INFO new client clientID kuWXCYxu version v1.2.1, rev 7510928, compiled at 2024-04-19 10:07:36 UTC ; nClients 1
2024-04-19 13:11:57 INFO new remotes list 1 clientID kuWXCYxu 127.0.0.1

nocc_client.log

2024-04-19 13:11:57 INFO nocc-daemon started in 2 ms
2024-04-19 13:11:57 INFO env: clientID kuWXCYxu ; user user ; num servers 1 ; ulimit -n 4096 ; num cpu 8 ; version v1.2.1, rev 7510928, compiled at 2024-04-19 10:07:36 UTC

Workarounds

  1. Add new line to the end of include file.
  2. Use NOCC_DISABLE_OWN_INCLUDES=1.

flag provided but not defined: -DGTEST_HAS_RTTI

akeFiles/LLVMSupportBlake3.dir/blake3_sse2_x86-64_unix.S.o -c /home/jack/codejk/llvm-project/llvm/lib/Support/BLAKE3/blake3_sse2_x86-64_unix.S
flag provided but not defined: -DGTEST_HAS_RTTI
Usage of /home/jack/cc/nocc:

  -version / -v
        Show version and exit.

Failed to build test/cmake1 with remove build server

Hi,

Thanks a lot for your project, really powerful!

I tested "nocc/tests/dt/cmake1" in master branch (commit: 73b26e5) with a remote build server.

CMake version: 3.18.6
OS (local and remote): Ubuntu 20.04

In the nocc/tests/dt/cmake1/build.sh, I adjust NOCC_SERVERS to the remote build server IP, and I encoutered the error below:

In file included from <command-line>:
/home/test_nocc/nocc/tests/dt/cmake1/build/CMakeFiles/example.dir/cmake_pch.hxx:5:10: fatal error: /home/test_nocc/nocc/tests/dt/cmake1/src/all-headers.h: No such file or directory
    5 | #include "/home/test_nocc/nocc/tests/dt/cmake1/src/all-headers.h"
      |          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
compilation terminated.

Do I need to do some preparations for the PCH headers? Currently I have to copy the header to the remote to fix the issue.

Thanks again for your power tool!

Add support for assembly files and make file extensions case insensitive

Suggested changes:

func isSourceFileName(fileName string) bool {
return strings.HasSuffix(fileName, ".cpp") ||
strings.HasSuffix(fileName, ".cc") ||
strings.HasSuffix(fileName, ".cxx") ||
strings.HasSuffix(fileName, ".c")
}
func isHeaderFileName(fileName string) bool {
return strings.HasSuffix(fileName, ".h") ||
strings.HasSuffix(fileName, ".hh") ||
strings.HasSuffix(fileName, ".hxx") ||
strings.HasSuffix(fileName, ".hpp")
}

->

func isSourceFileName(fileName string) bool {
	return strings.HasSuffix(strings.ToLower(fileName), ".cpp") ||
		strings.HasSuffix(strings.ToLower(fileName), ".cc") ||
		strings.HasSuffix(strings.ToLower(fileName), ".cxx") ||
		strings.HasSuffix(strings.ToLower(fileName), ".c") ||
		strings.HasSuffix(strings.ToLower(fileName), ".s")
}

func isHeaderFileName(fileName string) bool {
	return strings.HasSuffix(strings.ToLower(fileName), ".h") ||
		strings.HasSuffix(strings.ToLower(fileName), ".hh") ||
		strings.HasSuffix(strings.ToLower(fileName), ".hxx") ||
		strings.HasSuffix(strings.ToLower(fileName), ".hpp")
}

Checkout at Windows git failed

Checkout at Windows git failed

K:\>git clone https://github.com/VKCOM/nocc.git
Cloning into 'nocc'...
remote: Enumerating objects: 222, done.
remote: Counting objects: 100% (53/53), done.
remote: Compressing objects: 100% (36/36), done.
Receiving objects:  97% (216/222)sed 19 (delta 16), pack-reused 169
Receiving objects: 100% (222/222), 1.19 MiB | 5.39 MiB/s, done.
Resolving deltas: 100% (75/75), done.
error: invalid path 'tests/dt/dep1/dir with space/strange<>name()&!@#-*.h.inc'
fatal: unable to checkout working tree
warning: Clone succeeded, but checkout failed.
You can inspect what was checked out with 'git status'
and retry with 'git restore --source=HEAD :/'

Partial workaround

git config core.protectNTFS false

But still have

K:\nocc>git checkout
error: unable to create file tests/dt/dep1/dir with space/strange<>name()&!@#-*.h.inc: Invalid argument
D       tests/dt/dep1/dir with space/strange<>name()&!@#-*.h.inc
Your branch is up to date with 'origin/master'.

Reason: https://github.com/VKCOM/nocc/blob/master/tests/dt/dep1/dir%20with%20space/strange%3C%3Ename()%26!%40%23-*.h.inc

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.