About the author: Zihao Zhong, senior iOS engineer of Beitchat Technology

preface

In the previous article introducing CCache, I discussed how to use CCache to optimize the construction time of the application. In the comments, I received feedback from many friends that they encountered difficulties in the process of using CCache and could not successfully apply CCache. We have also encountered most of these problems in the integration process of Beitchat project. This paper mainly gives corresponding solutions to these problems, and gives some suggestions to optimize the application construction time from other aspects.

Improved cache hit ratio

You can run the ccache -s command to view the cache hit ratio of ccache. For CCache to really reduce compile time, the hit rate needs to be about 90%. As mentioned above, frequently caching miss is slower than not using CCache. Therefore, after integrating CCache, a cache hit ratio of 90% should be guaranteed to improve the construction speed. If the cache hit ratio is too low, log analysis should be performed to troubleshoot the cause.

Remove the Precompiled Prefix Header

The use of PCH files is the most common cause of poor cache hit ratio. Many projects import frequently used classes into PCH for convenience, and the contents of the PCH are appended to each file at compile time. Changes in the PCH contents will cause the contents of the entire file for the compiled object to change, thus invalidation of the CCache cache. If you’ve ever run a project that compiles a full version of one or two source files after you’ve changed them, check to see if there are too many import dependencies in the PCH. If you can’t remove the PCH, try to reduce the number of import files in the PCH.

Use the contents of the file as the Key for the compilation cache

Because the common git branch switch, pod update and other operations will cause a large number of file last edit time changes, if the file last edit time as the cache key, often see a large number of file cache failure after switching Git branch or POD update. By default, CCache uses the MD4 digest value of the file’s content as part of the cache key, as long as the file_stat_matches option is not included. Although it takes longer to calculate and compare the summary value of the file content than to simply compare the modification time and size of the file, it has been found through practice that using the file content as the key will have a more stable compilation optimization effect. The following is the CCache configuration that I am currently using with good effect:

#! /bin/sh
if type -p ccache >/dev/null 2>&1; then
  export CCACHE_MAXSIZE=10G
  export CCACHE_CPP2=true
  export CCACHE_HARDLINK=true
  export CCACHE_SLOPPINESS=file_macro,time_macros,include_file_mtime,include_file_ctime,pch_defines
  
  exec ccache /usr/bin/clang "$@"
else
  exec clang "$@"
fi
Copy the code

Enable CCache only on Release builds

During development with Debug configuration, Xcode comes with its own incremental compilation, except when building AdHoc or AppStore using a Release configuration. Therefore, enabling CCache in Debug mode is meaningless. Frequent file changes during development lead to low cache hit ratio and slow down daily development.

Disable CCache for a single Pod

Using CCache requires the Clang Module function to be turned off, and some third-party libraries use the @import syntax so that if the Clang Module is turned off, the @import syntax cannot be used, resulting in compilation errors. In this case, you can set a separate Pod without enabling CCache.

The Podfile configuration code below demonstrates both how to enable CCache only on Release builds and how not to use CCache for individual pods:

# Podfile

target 'YourApp' do # Replace with your target name
  post_install do |installer_representation|
    installer_representation.pods_project.targets.each do |target|
      ifconfig.name ! ='Debug'&& target.name ! ='SomePod' Replace with the name of the Pod you want to exclude
        config.build_settings['CC'] = '$(PODS_ROOT)/ccache-clang' # Replace with your ccache-clang file path
      end
    end
  end
end
  
Copy the code

Other optimization points

These optimizations have nothing to do with CCache, but I’ve tried them out and found some improvements in compilation speed and development experience, so they are listed together.

Adjust the maximum number of concurrent compilations

Update: After Xcode 9.3 was released, I tested it again with the same project and got similar results, but the differences were small. I recommend Benchmark before deciding whether to apply it to your own project.

I passed this order earlier

defaults write com.apple.dt.Xcode IDEBuildOperationMaxNumberOfConcurrentCompileTasks `sysctl -n hw.ncpu`
Copy the code

Setting the maximum concurrency at compile time to the number of CPU cores is not the best configuration. I first from The following article found: The best hardware to build with Swift is not what you took think | brought The British

Although the author stated that Xcode 9 had fixed this problem, I tested it with a pure Objective-C project and found the following:

Concurrency of compilation Average time to compile three times (seconds)
4 118.3
6 110.6
8 126.5

I was surprised to see that the compilation speed of 8 threads was worse than that of 4 threads. Although the difference is not huge, it does affect the compilation speed by adjusting the number of concurrent compilations. You can experiment with your own project on your own build machine to find the optimal configuration.

In addition, I also used the same project to test the performance of the New Build System of Xcode 9, and the results are as follows

Concurrency of compilation Average time to compile three times (seconds)
4 132.3
6 133.7
8 134.5

It is found that the New Build System is slightly slower than the old Build System, but the compilation process is more rigorous. For example, in the Copy Bundle Resources step, if an image resource file is found missing, it will be thrown as an error rather than a warning. If your project has already switched to the New Build System, I recommend that you continue to use it, after all, apple will do a lot of optimization for it in the future, and this performance issue should be resolved.

Avoid recursive lookup of header files

${PROJECT_DIR}/** was added in the Build Settings-> Header Search Paths file. This configuration causes files to be compiled recursively looking for header files throughout the project directory, significantly increasing the search time for header files, which can increase the compilation time of individual files by as much as 2-3 times.

Cleaning up obsolete code

No code compiles faster than “no code”, which is an obvious optimization, but can be a lot of work to implement. It is easy to create modules that are no longer used when your business is rapidly iterating and updating. Regular cleaning can reduce both project build time and installation package size. If you think the code will be useful in the future, you can simply remove references to the Xcode project and keep the files for reference.

Turn off warnings in the Release configuration

Clang generates a warning message and prints it to the console or log if it encounters non-standard code during compilation, such as unused variables. Processing this warning message also consumes some computing resources. Warning messages are recommended to be removed at development time, and inhibit_all_warnings! Can be used in Podfile if warnings come from third-party libraries integrated by CocoaPods. Options open. Baychat’s project shaved about 40 seconds off the compile time by doing this.

conclusion

Our project was able to reduce the average compile time from over 1300 seconds to less than 300 seconds, depending on the hit ratio of CCache, but overall it was significantly reduced. Hopefully these lessons will help your project.