Speeding up multi-architecture compilation by parallelizing your build

I’ve lately been working on getting our automated builds complete faster. Faster builds means shorter times between commits and new fresh builds, gated check-ins can be evaluated faster etc etc. In my specific case, I need to build and link A LOT of native C++ code for both x86, x64 and ARM, and the full process takes over 1.5 hours on my speedy desktop, and over 2 hours on the build server!

As part of this work, wanted the build server to only focus on the builds that were important for the product, but still have a solution that builds unit tests, test project etc for the devs to use day to day. Also because we build both AnyCPU .NET libraries and architecture-specific apps and native libraries, the build server would while building each architechture also build the AnyCPU builds over and over, and the build server also didn’t really need to build the unit tests as part of the production build either.

Now I could go about and create a separate build configuration, but I wanted to make sure the build configuration devs use day to day matches what the build server uses. So I opted for creating an “msbuild” file instead. This is essentially a little project file, that points to other projects to build. Here’s a small example of such a file:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">    
     <Target Name="BuildMyProduct">
       <MSBuild Projects="MyCoolApp\MyCoolClassLibrary.csproj" Properties="Platform=AnyCPU;Configuration=Release" Targets="Restore;Build" />
       <MSBuild Projects="MyCoolApp\MyCoolApp.csproj" Targets="Restore" />
       <MSBuild Projects="MyCoolApp\MyCoolApp.csproj" Properties="Platform=x86;Configuration=Release" Targets="Build" />
       <MSBuild Projects="MyCoolApp\MyCoolApp.csproj" Properties="Platform=x64;Configuration=Release" Targets="Build" />
       <MSBuild Projects="MyCoolApp\MyCoolApp.csproj" Properties="Platform=ARM;Configuration=Release" Targets="Build" />
     </Target>
</Project>

From a Visual Studio command prompt you can use msbuild to execute your ‘BuildMyProduct’ target:

           msbuild /t:BuildMyProduct

And the project will nuget restore and build first the class library, then build the app project 3 times for each architecture (with a single nuget restore before it). You can see from the output that things are built in the order specified, one after the other. Note how the same output for the app project essentially repeats 3 times:

SequentialBuild


The thing is, several of the compilation process is single-threaded, but these days we all have 4, 8, 16 etc cores to play with. Why not put them all to good use and speed up the build? Especially linking a lot of native C++ static libraries are mostly single threaded and can easily take a very long time. Why not use a CPU for each architecture?

We can accomplish this in msbuild by creating a group of projects with different properties, and use the “BuildInParallel” parameter. Here’s what that same project would then look like:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"
    <Target Name="BuildMyProduct">
       <ItemGroup>
         <MyCoolAppProject Include="MyCoolApp\MyCoolApp.csproj" AdditionalProperties="Platform=x86" />
         <MyCoolAppProject Include="MyCoolApp\MyCoolApp.csproj" AdditionalProperties="Platform=x64" />
         <MyCoolAppProject Include="MyCoolApp\MyCoolApp.csproj" AdditionalProperties="Platform=ARM" />
       </ItemGroup>
       <MSBuild Projects="MyCoolApp\MyCoolClassLibrary.csproj" Properties="Platform=AnyCPU;Configuration=Release" Targets="Restore;Build" />
       <MSBuild Projects="MyCoolApp\MyCoolApp.csproj" Targets="Restore" />
       <MSBuild Projects="@(MyCoolAppProject)" Properties="Configuration=Release" Targets="Build" BuildInParallel="True" />
     </Target>
</Project>

In this case we’ll add the /maxcpucount parameter to ensure msbuild uses all the available CPUs:

           msbuild /t:BuildMyProduct /maxcpucount

Build in parallel output below. Note hos the output is exactly the same, but now each architecture output more or less outputs in the same order and completes simultaneously.

ParallelBuild

But much more importantly, notice the 40% reduction in build time! And this is for a completely blank UWP app template. This is no small reduction. And remember if you have lots of native code to link, you can see even bigger saves. In my specific case, I have A LOT of static libraries to link, which takes between 22 and 35mins to just link depending on architecture. The entire build takes about 90mins to complete. When building in parallel, that’s “only” 38 minutes. This is HUGE for pushing out fresh setups to test, or enabling gated check-ins.


Now for UWP you might want to create a single bundle for all architectures, and you can do that with a single command that’ll build all architectures:

    <Target Name="BuildFullBundle">
       <MSBuild Projects="MyCoolApp\MyCoolApp.csproj" Targets="Restore" />
       <MSBuild Projects="MyCoolApp\MyCoolApp.csproj" Targets="Build" Properties="Configuration=Release;AppxBundle=Always;AppxBundlePlatforms=x86|x64|ARM" BuildInParallel="True" />
     </Target>

The interesting bit here is that msbuild doesn’t actually parallelize this build (though it probably should) as clearly visible in the output, and you get the same “slow” build time.

AppxBundleBuild