Standards conformance improvements to /Gw in Visual Studio version 17.5 Preview 2

Kyle Brady

The /Gw switch enables the linker to optimize global data to reduce binary size. As part of the 17.5 Preview 2 release a new flag, /Zc:checkGwOdr[-], has been added to improve C++ standards conformance when using /Gw. Previously, when using /Gw, certain One Definition Rule (ODR) violations were being ignored and would not cause an error. The new flag ensures that we do raise the appropriate errors. If you are currently using /Gw we recommend setting /Zc:checkGwOdr on your builds, as it’s currently off by default. This may change in a future major update. For a more detailed explanation of ODR, /Gw, and how this issue came about, read on below.

Let’s go through three definitions that we need for background here:

  1. First up are COMDATs, a short description is that COMDATs are extra segments that data can be placed in to enable the linker to potentially fold out said data from the binary. Importantly, these sections are flagged with a strategy for how duplicates are handled. For a deeper dive into the history of COMDATs, see Raymond’s blog post which covers their use and history.
  2. Next, just what does /Gw do? For a full explanation, check out the blog post announcing the switch, but for our purposes the flag enables the compiler to place global data into COMDATs. This lets us optimize out unreferenced globals, or merge identical globals via their COMDAT sections
  3. Finally, the One Definition Rule (ODR) as described on Wikipedia.

With the background out of the way we’ll go through a simple but common enough example:

odr.h:

#pragma once
// defining a global here is an ODR violation
// the initialization to 0 isn’t required to cause a violation
// but is needed to exhibit the incorrect /Gw behavior
int MyGlobal = 0;

bar.cpp:

#include "odr.h"
int bar() {
    return 2;
}

foo.cpp:

#include "odr.h"
int foo() {
   return 1;
}

extern int bar();

int main() {
   foo();
   bar();
}

Compiling this without /Gw leads to an error as we’d expect:

cl /nologo foo.cpp bar.cpp
foo.cpp
bar.cpp
Generating Code...
bar.obj : error LNK2005: "int MyGlobal" (?MyGlobal@@3HA) already defined in foo.obj
foo.exe : fatal error LNK1169: one or more multiply defined symbols found

Since we defined MyGlobal in odr.h, we end up with a definition in both foo.obj & bar.obj, causing the linker to report the ODR violation. Now, if we compile with /Gw:

cl /nologo /Gw foo.cpp bar.cpp
foo.cpp
bar.cpp
Generating Code...

We don’t end up with an error, so what happened? It ends up coming back to the COMDAT flags mentioned above. Looking at the obj headers, we can see that MyGlobal has indeed been placed in a COMDAT:

link.exe /dump /headers foo.obj
…
SECTION HEADER #3
    .bss name
    …
C0301080 flags
         Uninitialized Data
         COMDAT; sym= "int MyGlobal" (?MyGlobal@@3HA)
         4 byte align
         Read Write

What this doesn’t show is that this COMDAT has been flagged as PICKANY. So, when multiple definitions that would be candidates for merging are found the linker arbitrarily picks one of them and discards the rest. This is odd though, when /Gw is enabled this COMDAT was flagged with NOMATCH on creation. NOMATCH, as the name implies, means that the linker should raise an error if a duplicate is found, exactly what we want. So, what went wrong?

The key here is that the definition of MyGlobal includes an assignment to zero. This causes another optimization to kick in. Since this global is initialized to zero, we notice that it can be moved into the .bss section. Since we don’t have to store the data for this global if it’s in .bss, moving the COMDAT lets us reduce the object file size. Unfortunately, when we move the COMDAT the flag was being reset from NOMATCH to PICKANY, causing our bug.

As of 17.5 Preview 2 you can now use the new flag to make sure you’re not hiding these ODR violations on accident with /Gw:

cl /nologo /Gw /Zc:checkGwOdr foo.cpp bar.cpp
foo.cpp
bar.cpp
Generating Code...
bar.obj : error LNK2005: "int MyGlobal" (?MyGlobal@@3HA) already defined in foo.obj
foo.exe : fatal error LNK1169: one or more multiply defined symbols found

With the error exposed, we can make the global extern in our header, and move the definition to one of the cpp files to resolve the problem:

odr.h:

#pragma once
extern int MyGlobal;

foo.cpp:

#include "odr.h"
int MyGlobal = 0;
…
cl /nologo /Gw /Zc:checkGwOdr foo.cpp bar.cpp
foo.cpp
bar.cpp
Generating Code...

Alternatively, for C++17 and higher, you can use an inline specifier on the definition:

odr.h:

inline int MyGlobal = 0;

Usually fixing an ODR violation looks something like this, though not every case will be so simple. We encourage using /Zc:checkGwOdr to prevent these violations from creeping into your builds if you’re using /Gw. As this is a standards conformance issue, we may change the default behavior of /Gw to imply /Zc:checkGwOdr in a future release.

Send us your feedback

We hope you found these details interesting! If you have ideas for similar posts you’d like to see, please let us know. We are also interested in your feedback to continue to improve our tools. The comments below are open. Feedback can also be shared through Developer Community. You can also reach us on Twitter (@VisualC), or via email at visualcpp@microsoft.com.

 

Posted in C++

1 comment

Discussion is closed. Login to edit/delete existing comments.

  • hwang 3

    I really appreciate the efforts the c++ team has maded to improve Visual Studio’s standard conformance ability. However, so many new compiler bugs have been introduced since VS17.2 preview. I have reported more than 40 new compiler bugs since VS17.2 preview is released. 12 of them are still under investigation and 13 of them are still pending release. With VS17.4 we have 25 types of compiler bug workarounds to make our code base compile. When I’m expecting VS17.5 could fix half of them and improve our confidence about VC++ compiler, the new VS17.5 preview 2 introduced more bugs again. This is basically what we have experienced since VS17.2 preview. New bugs kept popping up after new releases and faster than what we could report. How can we update to a new version of Visual Studio if the compiler bug number is not convergent?

    Visual Studio becomes especially buggy when it comes to (multi-layer) lambda, unevaluated context and templates, and the combination of the them. When you introduce a new change/feature or fix a bug, could you please introduce enough unit tests to verify the change/fix? For example if a change could fix the example in a ticket, could you please also expand the example to cover more use cases, e.g. inside more layers of (possibly generic) lambda? I’m sure the c++ team could come up a way to cover more potential use cases. Thank you! Please make Visual Studio release more stable in the future!

Feedback usabilla icon