• Josh Bleecher Snyder's avatar
    cmd/compile: add initial backend concurrency support · 756b9ce3
    Josh Bleecher Snyder authored
    This CL adds initial support for concurrent backend compilation.
    
    BACKGROUND
    
    The compiler currently consists (very roughly) of the following phases:
    
    1. Initialization.
    2. Lexing and parsing into the cmd/compile/internal/syntax AST.
    3. Translation into the cmd/compile/internal/gc AST.
    4. Some gc AST passes: typechecking, escape analysis, inlining,
       closure handling, expression evaluation ordering (order.go),
       and some lowering and optimization (walk.go).
    5. Translation into the cmd/compile/internal/ssa SSA form.
    6. Optimization and lowering of SSA form.
    7. Translation from SSA form to assembler instructions.
    8. Translation from assembler instructions to machine code.
    9. Writing lots of output: machine code, DWARF symbols,
       type and reflection info, export data.
    
    Phase 2 was already concurrent as of Go 1.8.
    
    Phase 3 is planned for eventual removal;
    we hope to go straight from syntax AST to SSA.
    
    Phases 5–8 are per-function; this CL adds support for
    processing multiple functions concurrently.
    The slowest phases in the compiler are 5 and 6,
    so this offers the opportunity for some good speed-ups.
    
    Unfortunately, it's not quite that straightforward.
    In the current compiler, the latter parts of phase 4
    (order, walk) are done function-at-a-time as needed.
    Making order and walk concurrency-safe proved hard,
    and they're not particularly slow, so there wasn't much reward.
    To enable phases 5–8 to be done concurrently,
    when concurrent backend compilation is requested,
    we complete phase 4 for all functions
    before starting later phases for any functions.
    
    Also, in reality, we automatically generate new
    functions in phase 9, such as method wrappers
    and equality and has routines.
    Those new functions then go through phases 4–8.
    This CL disables concurrent backend compilation
    after the first, big, user-provided batch of
    functions has been compiled.
    This is done to keep things simple,
    and because the autogenerated functions
    tend to be small, few, simple, and fast to compile.
    
    USAGE
    
    Concurrent backend compilation still defaults to off.
    To set the number of functions that may be backend-compiled
    concurrently, use the compiler flag -c.
    In future work, cmd/go will automatically set -c.
    
    Furthermore, this CL has been intentionally written
    so that the c=1 path has no backend concurrency whatsoever,
    not even spawning any goroutines.
    This helps ensure that, should problems arise
    late in the development cycle,
    we can simply have cmd/go set c=1 always,
    and revert to the original compiler behavior.
    
    MUTEXES
    
    Most of the work required to make concurrent backend
    compilation safe has occurred over the past month.
    This CL adds a handful of mutexes to get the rest of the way there;
    they are the mutexes that I didn't see a clean way to avoid.
    Some of them may still be eliminable in future work.
    
    In no particular order:
    
    * gc.funcsymsmu. The global funcsyms slice is populated
      lazily when we need function symbols for closures.
      This occurs during gc AST to SSA translation.
      The function funcsym also does a package lookup,
      which is a source of races on types.Pkg.Syms;
      funcsymsmu also covers that package lookup.
      This mutex is low priority: it adds a single global,
      it is in an infrequently used code path, and it is low contention.
      Since funcsyms may now be added in any order,
      we must sort them to preserve reproducible builds.
    
    * gc.largeStackFramesMu. We don't discover until after SSA compilation
      that a function's stack frame is gigantic.
      Recording that error happens basically never,
      but it does happen concurrently.
      Fix with a low priority mutex and sorting.
    
    * obj.Link.hashmu. ctxt.hash stores the mapping from
      types.Syms (compiler symbols) to obj.LSyms (linker symbols).
      It is accessed fairly heavily through all the phases.
      This is the only heavily contended mutex.
    
    * gc.signatlistmu. The global signatlist map is
      populated with types through several of the concurrent phases,
      including notably via ngotype during DWARF generation.
      It is low priority for removal.
    
    * gc.typepkgmu. Looking up symbols in the types package
      happens a fair amount during backend compilation
      and DWARF generation, particularly via ngotype.
      This mutex helps us to avoid a broader mutex on types.Pkg.Syms.
      It has low-to-moderate contention.
    
    * types.internedStringsmu. gc AST to SSA conversion and
      some SSA work introduce new autotmps.
      Those autotmps have their names interned to reduce allocations.
      That interning requires protecting types.internedStrings.
      The autotmp names are heavily re-used, and the mutex
      overhead and contention here are low, so it is probably
      a worthwhile performance optimization to keep this mutex.
    
    TESTING
    
    I have been testing this code locally by running
    'go install -race cmd/compile'
    and then doing
    'go build -a -gcflags=-c=128 std cmd'
    for all architectures and a variety of compiler flags.
    This obviously needs to be made part of the builders,
    but it is too expensive to make part of all.bash.
    I have filed #19962 for this.
    
    REPRODUCIBLE BUILDS
    
    This version of the compiler generates reproducible builds.
    Testing reproducible builds also needs automation, however,
    and is also too expensive for all.bash.
    This is #19961.
    
    Also of note is that some of the compiler flags used by 'toolstash -cmp'
    are currently incompatible with concurrent backend compilation.
    They still work fine with c=1.
    Time will tell whether this is a problem.
    
    NEXT STEPS
    
    * Continue to find and fix races and bugs,
      using a combination of code inspection, fuzzing,
      and hopefully some community experimentation.
      I do not know of any outstanding races,
      but there probably are some.
    * Improve testing.
    * Improve performance, for many values of c.
    * Integrate with cmd/go and fine tune.
    * Support concurrent compilation with the -race flag.
      It is a sad irony that it does not yet work.
    * Minor code cleanup that has been deferred during
      the last month due to uncertainty about the
      ultimate shape of this CL.
    
    PERFORMANCE
    
    Here's the buried lede, at last. :)
    
    All benchmarks are from my 8 core 2.9 GHz Intel Core i7 darwin/amd64 laptop.
    
    First, going from tip to this CL with c=1 has almost no impact.
    
    name        old time/op       new time/op       delta
    Template          195ms ± 3%        194ms ± 5%    ~     (p=0.370 n=30+29)
    Unicode          86.6ms ± 3%       87.0ms ± 7%    ~     (p=0.958 n=29+30)
    GoTypes           548ms ± 3%        555ms ± 4%  +1.35%  (p=0.001 n=30+28)
    Compiler          2.51s ± 2%        2.54s ± 2%  +1.17%  (p=0.000 n=28+30)
    SSA               5.16s ± 3%        5.16s ± 2%    ~     (p=0.910 n=30+29)
    Flate             124ms ± 5%        124ms ± 4%    ~     (p=0.947 n=30+30)
    GoParser          146ms ± 3%        146ms ± 3%    ~     (p=0.150 n=29+28)
    Reflect           354ms ± 3%        352ms ± 4%    ~     (p=0.096 n=29+29)
    Tar               107ms ± 5%        106ms ± 3%    ~     (p=0.370 n=30+29)
    XML               200ms ± 4%        201ms ± 4%    ~     (p=0.313 n=29+28)
    [Geo mean]        332ms             333ms       +0.10%
    
    name        old user-time/op  new user-time/op  delta
    Template          227ms ± 5%        225ms ± 5%    ~     (p=0.457 n=28+27)
    Unicode           109ms ± 4%        109ms ± 5%    ~     (p=0.758 n=29+29)
    GoTypes           713ms ± 4%        721ms ± 5%    ~     (p=0.051 n=30+29)
    Compiler          3.36s ± 2%        3.38s ± 3%    ~     (p=0.146 n=30+30)
    SSA               7.46s ± 3%        7.47s ± 3%    ~     (p=0.804 n=30+29)
    Flate             146ms ± 7%        147ms ± 3%    ~     (p=0.833 n=29+27)
    GoParser          179ms ± 5%        179ms ± 5%    ~     (p=0.866 n=30+30)
    Reflect           431ms ± 4%        429ms ± 4%    ~     (p=0.593 n=29+30)
    Tar               124ms ± 5%        123ms ± 5%    ~     (p=0.140 n=29+29)
    XML               243ms ± 4%        242ms ± 7%    ~     (p=0.404 n=29+29)
    [Geo mean]        415ms             415ms       +0.02%
    
    name        old obj-bytes     new obj-bytes     delta
    Template           382k ± 0%         382k ± 0%    ~     (all equal)
    Unicode            203k ± 0%         203k ± 0%    ~     (all equal)
    GoTypes           1.18M ± 0%        1.18M ± 0%    ~     (all equal)
    Compiler          3.98M ± 0%        3.98M ± 0%    ~     (all equal)
    SSA               8.28M ± 0%        8.28M ± 0%    ~     (all equal)
    Flate              230k ± 0%         230k ± 0%    ~     (all equal)
    GoParser           287k ± 0%         287k ± 0%    ~     (all equal)
    Reflect           1.00M ± 0%        1.00M ± 0%    ~     (all equal)
    Tar                190k ± 0%         190k ± 0%    ~     (all equal)
    XML                416k ± 0%         416k ± 0%    ~     (all equal)
    [Geo mean]         660k              660k       +0.00%
    
    Comparing this CL to itself, from c=1 to c=2
    improves real times 20-30%, costs 5-10% more CPU time,
    and adds about 2% alloc.
    The allocation increase comes from allocating more ssa.Caches.
    
    name       old time/op       new time/op       delta
    Template         202ms ± 3%        149ms ± 3%  -26.15%  (p=0.000 n=49+49)
    Unicode         87.4ms ± 4%       84.2ms ± 3%   -3.68%  (p=0.000 n=48+48)
    GoTypes          560ms ± 2%        398ms ± 2%  -28.96%  (p=0.000 n=49+49)
    Compiler         2.46s ± 3%        1.76s ± 2%  -28.61%  (p=0.000 n=48+46)
    SSA              6.17s ± 2%        4.04s ± 1%  -34.52%  (p=0.000 n=49+49)
    Flate            126ms ± 3%         92ms ± 2%  -26.81%  (p=0.000 n=49+48)
    GoParser         148ms ± 4%        107ms ± 2%  -27.78%  (p=0.000 n=49+48)
    Reflect          361ms ± 3%        281ms ± 3%  -22.10%  (p=0.000 n=49+49)
    Tar              109ms ± 4%         86ms ± 3%  -20.81%  (p=0.000 n=49+47)
    XML              204ms ± 3%        144ms ± 2%  -29.53%  (p=0.000 n=48+45)
    
    name       old user-time/op  new user-time/op  delta
    Template         246ms ± 9%        246ms ± 4%     ~     (p=0.401 n=50+48)
    Unicode          109ms ± 4%        111ms ± 4%   +1.47%  (p=0.000 n=44+50)
    GoTypes          728ms ± 3%        765ms ± 3%   +5.04%  (p=0.000 n=46+50)
    Compiler         3.33s ± 3%        3.41s ± 2%   +2.31%  (p=0.000 n=49+48)
    SSA              8.52s ± 2%        9.11s ± 2%   +6.93%  (p=0.000 n=49+47)
    Flate            149ms ± 4%        161ms ± 3%   +8.13%  (p=0.000 n=50+47)
    GoParser         181ms ± 5%        192ms ± 2%   +6.40%  (p=0.000 n=49+46)
    Reflect          452ms ± 9%        474ms ± 2%   +4.99%  (p=0.000 n=50+48)
    Tar              126ms ± 6%        136ms ± 4%   +7.95%  (p=0.000 n=50+49)
    XML              247ms ± 5%        264ms ± 3%   +6.94%  (p=0.000 n=48+50)
    
    name       old alloc/op      new alloc/op      delta
    Template        38.8MB ± 0%       39.3MB ± 0%   +1.48%  (p=0.008 n=5+5)
    Unicode         29.8MB ± 0%       30.2MB ± 0%   +1.19%  (p=0.008 n=5+5)
    GoTypes          113MB ± 0%        114MB ± 0%   +0.69%  (p=0.008 n=5+5)
    Compiler         443MB ± 0%        447MB ± 0%   +0.95%  (p=0.008 n=5+5)
    SSA             1.25GB ± 0%       1.26GB ± 0%   +0.89%  (p=0.008 n=5+5)
    Flate           25.3MB ± 0%       25.9MB ± 1%   +2.35%  (p=0.008 n=5+5)
    GoParser        31.7MB ± 0%       32.2MB ± 0%   +1.59%  (p=0.008 n=5+5)
    Reflect         78.2MB ± 0%       78.9MB ± 0%   +0.91%  (p=0.008 n=5+5)
    Tar             26.6MB ± 0%       27.0MB ± 0%   +1.80%  (p=0.008 n=5+5)
    XML             42.4MB ± 0%       43.4MB ± 0%   +2.35%  (p=0.008 n=5+5)
    
    name       old allocs/op     new allocs/op     delta
    Template          379k ± 0%         378k ± 0%     ~     (p=0.421 n=5+5)
    Unicode           322k ± 0%         321k ± 0%     ~     (p=0.222 n=5+5)
    GoTypes          1.14M ± 0%        1.14M ± 0%     ~     (p=0.548 n=5+5)
    Compiler         4.12M ± 0%        4.11M ± 0%   -0.14%  (p=0.032 n=5+5)
    SSA              9.72M ± 0%        9.72M ± 0%     ~     (p=0.421 n=5+5)
    Flate             234k ± 1%         234k ± 0%     ~     (p=0.421 n=5+5)
    GoParser          316k ± 1%         315k ± 0%     ~     (p=0.222 n=5+5)
    Reflect           980k ± 0%         979k ± 0%     ~     (p=0.095 n=5+5)
    Tar               249k ± 1%         249k ± 1%     ~     (p=0.841 n=5+5)
    XML               392k ± 0%         391k ± 0%     ~     (p=0.095 n=5+5)
    
    From c=1 to c=4, real time is down ~40%, CPU usage up 10-20%, alloc up ~5%:
    
    name       old time/op       new time/op       delta
    Template         203ms ± 3%        131ms ± 5%  -35.45%  (p=0.000 n=50+50)
    Unicode         87.2ms ± 4%       84.1ms ± 2%   -3.61%  (p=0.000 n=48+47)
    GoTypes          560ms ± 4%        310ms ± 2%  -44.65%  (p=0.000 n=50+49)
    Compiler         2.47s ± 3%        1.41s ± 2%  -43.10%  (p=0.000 n=50+46)
    SSA              6.17s ± 2%        3.20s ± 2%  -48.06%  (p=0.000 n=49+49)
    Flate            126ms ± 4%         74ms ± 2%  -41.06%  (p=0.000 n=49+48)
    GoParser         148ms ± 4%         89ms ± 3%  -39.97%  (p=0.000 n=49+50)
    Reflect          360ms ± 3%        242ms ± 3%  -32.81%  (p=0.000 n=49+49)
    Tar              108ms ± 4%         73ms ± 4%  -32.48%  (p=0.000 n=50+49)
    XML              203ms ± 3%        119ms ± 3%  -41.56%  (p=0.000 n=49+48)
    
    name       old user-time/op  new user-time/op  delta
    Template         246ms ± 9%        287ms ± 9%  +16.98%  (p=0.000 n=50+50)
    Unicode          109ms ± 4%        118ms ± 5%   +7.56%  (p=0.000 n=46+50)
    GoTypes          735ms ± 4%        806ms ± 2%   +9.62%  (p=0.000 n=50+50)
    Compiler         3.34s ± 4%        3.56s ± 2%   +6.78%  (p=0.000 n=49+49)
    SSA              8.54s ± 3%       10.04s ± 3%  +17.55%  (p=0.000 n=50+50)
    Flate            149ms ± 6%        176ms ± 3%  +17.82%  (p=0.000 n=50+48)
    GoParser         181ms ± 5%        213ms ± 3%  +17.47%  (p=0.000 n=50+50)
    Reflect          453ms ± 6%        499ms ± 2%  +10.11%  (p=0.000 n=50+48)
    Tar              126ms ± 5%        149ms ±11%  +18.76%  (p=0.000 n=50+50)
    XML              246ms ± 5%        287ms ± 4%  +16.53%  (p=0.000 n=49+50)
    
    name       old alloc/op      new alloc/op      delta
    Template        38.8MB ± 0%       40.4MB ± 0%   +4.21%  (p=0.008 n=5+5)
    Unicode         29.8MB ± 0%       30.9MB ± 0%   +3.68%  (p=0.008 n=5+5)
    GoTypes          113MB ± 0%        116MB ± 0%   +2.71%  (p=0.008 n=5+5)
    Compiler         443MB ± 0%        455MB ± 0%   +2.75%  (p=0.008 n=5+5)
    SSA             1.25GB ± 0%       1.27GB ± 0%   +1.84%  (p=0.008 n=5+5)
    Flate           25.3MB ± 0%       26.9MB ± 1%   +6.31%  (p=0.008 n=5+5)
    GoParser        31.7MB ± 0%       33.2MB ± 0%   +4.61%  (p=0.008 n=5+5)
    Reflect         78.2MB ± 0%       80.2MB ± 0%   +2.53%  (p=0.008 n=5+5)
    Tar             26.6MB ± 0%       27.9MB ± 0%   +5.19%  (p=0.008 n=5+5)
    XML             42.4MB ± 0%       44.6MB ± 0%   +5.20%  (p=0.008 n=5+5)
    
    name       old allocs/op     new allocs/op     delta
    Template          380k ± 0%         379k ± 0%   -0.39%  (p=0.032 n=5+5)
    Unicode           321k ± 0%         321k ± 0%     ~     (p=0.841 n=5+5)
    GoTypes          1.14M ± 0%        1.14M ± 0%     ~     (p=0.421 n=5+5)
    Compiler         4.12M ± 0%        4.14M ± 0%   +0.52%  (p=0.008 n=5+5)
    SSA              9.72M ± 0%        9.76M ± 0%   +0.37%  (p=0.008 n=5+5)
    Flate             234k ± 1%         234k ± 1%     ~     (p=0.690 n=5+5)
    GoParser          316k ± 0%         317k ± 1%     ~     (p=0.841 n=5+5)
    Reflect           981k ± 0%         981k ± 0%     ~     (p=1.000 n=5+5)
    Tar               250k ± 0%         249k ± 1%     ~     (p=0.151 n=5+5)
    XML               393k ± 0%         392k ± 0%     ~     (p=0.056 n=5+5)
    
    Going beyond c=4 on my machine tends to increase CPU time and allocs
    without impacting real time.
    
    The CPU time numbers matter, because when there are many concurrent
    compilation processes, that will impact the overall throughput.
    
    The numbers above are in many ways the best case scenario;
    we can take full advantage of all cores.
    Fortunately, the most common compilation scenario is incremental
    re-compilation of a single package during a build/test cycle.
    
    Updates #15756
    
    Change-Id: I6725558ca2069edec0ac5b0d1683105a9fff6bea
    Reviewed-on: https://go-review.googlesource.com/40693Reviewed-by: 's avatarMatthew Dempsky <mdempsky@google.com>
    Reviewed-by: 's avatarRobert Griesemer <gri@golang.org>
    Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
    TryBot-Result: Gobot Gobot <gobot@golang.org>
    756b9ce3
dcl.go 27.8 KB