понедельник, 21 октября 2013 г.

Visual C++: PCH is not for Precompiled Hell

Recently I have faced the problem with Intellisense performance in Visual Studio 2012 for C++ , caused by Precompiled Headers feature being disabled. After eye-parsing some pages on the web including MSDN itself, MSDN blogs, stackoverflow and some other, I have managed to make the thing work fast enough, so that it doesn’t prevent me from coding. Still I find both the issue as well as the solution to it to be pretty undocumented and will thus explain my findings here in the form of a walkthrough.

The last night I decided to explore Cinder – a C++ library for ‘professional-quality creative coding’ as they put it on the website. For me this stands for graphics in the first place, so I wanted to remember how one makes primitives appear on the screen, leverages power of translation and rotation matrices and so on. I had Visual Studio 2012 Express for Desktop installed already, so I was sure everything will go pretty well and by midnight there will be something like a beautiful tetrahedron moving nicely across my display. Unfortunately, the things have taken a different road.

Soon after cloning and building the github repository I faced a dread beast. It jumped at me as soon as I started writing the first method: Intellisense was scanning included files. Obviously, no hints were available from it while it moved from 0 to 3000-some files parsed. Even worse, until the end of this process, which took a minute or two, the overall performance of the IDE was awful: it would hang from time to time, output text with a considerable lag and so on. It would be affordable should it happen only once – I am a patient person, I would have waited. In reality, I have found that Visual Studio comes back to this exercise from time to time – maybe once in 20 minutes, maybe every 5 – such state of affairs turned Sunday-evening coding into a horror.

The first article on the web, which was related to my problem, spoke of Precompiled Headers. As this nice entry by Andy Rich explains, Intellisense uses its own kind of PCH to be fast. Andy pointed out that, in case one experiences problems with Intellisense performance it is most likely caused by PCHs being not generated or being generated improperly. In my particular case the feature was turned off, because TinderBox  – Cinder's tool for creating Visual Studio projects – doesn’t enable it by default. However, the process of enabling it and making Intellisense for C++ nice and workable is not that straightforward. Below are the concrete steps that I have taken to make Visual Studio generate and use precompiled headers for my project.
  1. First of all, we need to tell our project to employ PCH. The corresponding settings are located in the project properties (right click project and choose ‘Properties’) under Configuration Properties -> C/C++ -> Precompiled Headers. There we have only 3 settings, which is pretty good. Precompiled Header is a flag, determining whether the environment will try to create or use (or neither) PCH for the selected piece of solution (HelloCinder project in my case). We want ‘Use’ option (/Yu) here. The next setting is Precompiled Header File – that’s a traditional C++ header which, as I get it, determines what should be there in the PCH and, if included in a particular translation unit, serves to tell environment to use the PCH. The default value is ‘stdafx.h’ and this will satisfy us most of the time. Lastly, Precompiled Header Output File stores the path to the actual precompiled header file, which allows for faster compilation. I’ve set it to the ‘$(SolutionDir)$(TargetName).pch’ which won’t be a good idea in case of multiple projects in solution but is OK for now. (I won't comment the names chosen by Microsoft for these settings, no.)

  2. If we try to build the project after completing this setup, there will be a little surprise – Studio will spit out something like this: “error C1010: unexpected end of file while looking for precompiled header. Did you forget to add '#include "stdafx.h"' to your source?” What this tells us is that we have to include our "stdafx.h" header in the files with source code to use PCH. Well, let’s include one. (Note: the #include directive corresponding to the PCH must be the first #include in the source file.) Since I had only one .cpp, this stage was easy, although upon doing this I arrived at 2 new errors: “error C1083: Cannot open source file: 'stdafx.cpp': No such file or directory” and “IntelliSense: cannot open source file "stdafx.h"” Whoa, there is something about Intellisense, so we must be on the right way. Neither of the errors is surprising actually: we told VS to use PCH when it sees “stdafx.h”, but we didn’t provide the file itself.

  3. So the next step is to add an “stdafx.h” header to the project (right click the ‘Header Files’ folder under the project, click ‘Add’, select ‘Header file’ and specify “stdafx.h” as its name – the same as with any other header). The reasonable question would be, what to put in it. Basically, there should go include directives which make life difficult for both Intellisense and compiler – these will determine what the environment would include into the precompiled header. Below are the includes which give me the required Cinder stuff and those, which actually make Intellisense blow up, because there is quite a lot things in there – I placed them into "stdafx.h":

    #include "cinder/app/AppNative.h"
    #include "cinder/app/RendererDx.h"
    #include "cinder/dx/dx.h"

  4.  So after adding the header I try to build the project once more and get another error: “error C1083: Cannot open precompiled header file: '{your path here}': No such file or directory”. This way it tries to communicate to me the idea that I asked it to use a PCH, but didn’t explain how should it get it. Frustrating? Maybe a bit, but be patient – we are almost there. To actually produce a precompiled header from our regular “stdafx.h”, we should do some magic. First of all we add an “stdafx.cpp” file containing a single line: #include “stdafx.h”. Then we have to tell VS to produce a PCH from it: we go to the properties of this file (right click on “stdafx.cpp” and chose 'Properties') and navigate to the familiar tab Configuration Properties -> C/C++ -> Precompiled Headers. There we should see the same settings, which we entered for our project on the step 1. Here it is: change the value of the Precompiled Header flag from ‘Use’ to ‘Create’, save and build the project. Normally, this should bring an end to the nightmare – in many cases it will.  


  5. I have faced an additional issue in the form of the following errors: “error C1076: compiler limit : internal heap limit reached; use /Zm to specify a higher limit” and “error C3859: virtual memory range for PCH exceeded; please recompile with a command line option of '-Zm119' or greater”. Well, at least this time it says what’s the issue and what can one do about it. It turns out that I have tried to push too much into the environment’s memory. To fix this we can do precisely what VS suggests: navigate to the project's Configuration Properties -> C/C++ -> Command Line. There is an Additional Options box at the bottom, where we can add the -Zm120 option.

After performing these 5 steps I was finally able to build the project and find a HelloCinder.pch file in my solution folder. Moreover, this worked like a charm (quite complex one, in fact) making Intellisense fast and usable through creating its own precompiled headers in the ipch folder. That’s what happiness is!

Concluding, let’s overview what we have done. First, we instructed Visual Studio to use a precompiled header for our project in its preferences. Then we told it what should get included in the PCH by means of the “stdafx.h” header and included it into our source files to let Studio know that the PCH should actually be leveraged for them. Finally we had to specify that the environment should create the precompiled header through setting proper options for the fresh “stdafx.cpp” file. In the end we had to deal with virtual memory limit using the -Zm command line option.

I must admit that there is much that I don’t understand about both precompiled headers and the way Intellisense uses them. This said, in the above guide I might have misrepresented something and given advice which is far from best practice. Still, I pursued the goal of describing a way to deal with Intellisense performance issues in detail, because I have found it quite difficult to figure out, despite the presence of the mentioned quite informative article by Andy Rich and other materials. Hope this helps. I would be glad to see any comments pointing out the places where I am wrong.

Edit: soon after finishing the post I have found this thorough description of the matter on stackoverflow.

Комментариев нет:

Отправить комментарий