shared/libebm/IMPORTANT.md
We use a Directory.Build.targets file to wildcard include files in the compute directory in all builds of each compute type https://docs.microsoft.com/en-us/cpp/build/reference/vcxproj-files-and-wildcards?view=msvc-160
BIG CHANGE: we need to create a new "separate" library that defines all the separate stuff below... we can use this in order to create different SIMD compilations but also we'll want to be able to run different GPU compilers on the stuff in that directory, so imagine compiling against CUDA, OpenCL and Metal all with different compilers etc. Since we're putting this into it's own library we can create separate .cpp files and we can use the non-anonymous namespace trick to keep them all very very separate when we link them all together. Our VS studio solution should be set to the default non-simd version and we can optionally use others, and maybe even CUDA etc compilations. If we use only C interfaces between internal libraries then there's a higher chance we can use different compilers to build modules and link them together separately afterwards since .o files are more standardized for C than C++.
C++ is in many ways a very flawed language. One of the most insidious aspects are inadvertant violations of the One Definition Rule (ODR) and related issues of the Application Binary Interface (ABI) not being standardized in C++. The worst part of ODR violations is that the compiler/linker is not required to detect them, and they can lead to surprising crashes that can be very difficult to debug. Some links which provide more context:
https://en.wikipedia.org/wiki/One_Definition_Rule https://en.cppreference.com/w/cpp/language/definition https://akrzemi1.wordpress.com/2016/11/28/the-one-definition-rule/ https://gieseanw.wordpress.com/2018/10/30/oops-i-violated-odr-again/ https://www.drdobbs.com/c-theory-and-practice/184403437 https://devblogs.microsoft.com/cppblog/diagnosing-hidden-odr-violations-in-visual-c-and-fixing-lnk2022/
In the blog from Andy Rich above, he gives an example of two *.cpp files with a shared header that are linked together, but are compiled with different options like this:
cl main.cpp /Zp2 /c cl loader.cpp /Zp1 /c link main.obj loader.obj
Which causes ODR violations between the *.cpp files because the /Zp2 and /Zp1 options control data alignment in class/struct definitions, so the two *.cpp files have different interpretations of how the class should be layed out and when an object of that class is passed between the cpp files they break. The moral of this story is be very very careful when compiling separate translation units together that have different options. This particular packing issue would have been a problem between *.c files as well, but the problem is more pronounced in C++ because C at least gives you guarantees regarding POD data structures, wheras C++ provides no guarantees that different classes will be layed out in memory in identical ways between different compilers, or different versions of the same compiler, or if you compile different .cpp files with different compiler options. The only real solution is to only use POD data structures between between translation units when the translation units are not compiled identically, including with identical compiler switches.
For simple programs this isn't usually an issue, but it starts to become an issue when linking libraries together to form more complicated programs. Most people first encounter this when they try to link together .dll or .so files compiled by either different compilers or different versions of the same compiler. For anything C++ this usually breaks as name mangling often isn't compatible, but this is just one symptom of a larger problem that hides in the shadows, namely that C++ libraries pretty much guarantee ODR violations.
The specific section that this violates in C++ is under "One Definition rule" where it says " There can be more than one definition of a class type... (list other stuff).. in a program provided that each definition appears in a different translation unit, and provided the definitions satisfy the following requirements:
The problem is that "the same sequence of tokens" in practice means it needs to be compiled with identical compiler switches to get this guarantee that multiple classes can be shared between translation units.
Unfortunately for us, we have two problems that need to be resolved. In InterpretML most of our .cpp files can be compiled by a single version of a single compiler with identical compiler switches, BUT if we want to gracefully handle different versions of SIMD operations within the same library we need to use different compiler switches on some translation units to enable different SIMD operations. We can then use dynamic detection to pick the right SIMD function to call. That breaks any guarantees of being able to share class definitions. Most packages solve this by building separate dynamically loaded libraries (.dll or .so) and attempting to hide their internal names (which is esoteric in linux where symbols are public by default). We could also use this method to solve our SIMD issues, but we also want to enable usage in GPUs which is a harder but related problem. Anything that we want to handle with SIMD for performance reasons is also something we probably want to push to the GPU or distributed system if available. In the case of GPU we'll be re-compiling our code with a GPU compiler which is very hard because we then can't share anything really between the modules. We'll probably even have to translate our data between little endian and big endian, nevermind worrying about class definition issues. To solve this we really need to have a very strong separation between our normal InterpretML code and the code meant to be SIMDed and/or GPUed and/or distributed. Since the only ABI interface that is cross compiler compatible is the C interface and any data structures need to be POD and/or basic types we need to restrict ourselves to the C formats for a significant part of the codebase and we need to be VERY careful how we share things.
For class definitions, functions, enumerations, variables, etc that don't need to be shared accross translation units (*.cpp files) it's usually best to use internal linkage on those things. We can do this for functions and variables by declaring them "static", which we generally also like to combine with INLINE and/or constexpr when appropriate. class definitions are more problematic as static doesn't give member variables and functions internal linkage (static just means share this accross objects in the context of classes), so we need to use anonymous namespaces to give class definitions internal linkage.
The C++ standard header files are an interesting case. In theory, since we're using separate compilation flags we should be worried that we might be able to generate ODR violations when we compile them into separate translation units and then shared within a "program" (C++ standardization term), in the same way that we generate the ODR violation by changing the packing, but in practice the C++ standard headers are created by the compiler vendor and the compiler vendor can add additional guarantees above the C++ standard that other library developers can't get in a cross compatible way. This happens in practice as the C++ standard library gets linked in with your program and since you can set compiler flags it's possible to have different compiler flags than what the C++ standard headers were compiled with. Hopefully the C++ compiler provider guarantees these kinds of mismatches are ok within their ecosystem. In practice, we don't have a realistic resolution anyways since we can't put the C++ standard headers into namespaces as then everything in the standard libraries would be considered by the compiler to be different objects from the entities in the standard library, so we just have to assume the compiler writer is doing the right thing outside of any guarantees that the C++ standard gives. It kind of sucks that the only C++ library that can provide this guarantee is the standard library and all others need to use extern "C" functions and POD structures and/or header only libraries (or mixes of these!).
For things that need to be shared between compilers and/or memory regions, we need to use POD data structures, extern "C" functions, and completely separate objects. To avoid issues we separate the codebase into the following: