Related
I'm very new to compile and compilers and i have some questions:
Does the binary generated by compiler A is different from compiler B? (all other conditions for example os and architecture and ... are the same). Why? How they are different?
Does the binary generated from compiling A language differs from the binary generated from compiling B language? (all other conditions for example compiler, os and architecture and ... are the same). in other words is there any relation or dependency between the source language from which the binary generated? Why? If yes, how they are related?
Yes to all of your questions.
You can even run a compiler twice on the same computer with the same source code, and the compiler can generate different binary outputs.
does the binary generated by compiler A is different from compiler B?
There are different ways to solve the same problem. In the human world, if you need to travel one city block north and one city block east, there are two possible paths. You can go north then east, or you can go east then north. It's doesn't matter which way you go, as long as you get to your destination.
Similarly, if you tell the compiler to add 3 and 5, there are multiple ways to solve this problem. It doesn't matter what the compiler does, as long as the result is the same.
Compiler A: Start with 3, then add 5.
Compiler B: Start with 5, then add 3.
Compiler C: Compute 3+5=8 at compile-time, then just load 8 into a register a run-time.
Compiler D: Start with 0, bitwise invert, left-bitshift 3, bitwise invert, add 1.
All of these produce the same result, and depending on the computer architecture and the compiler's settings, one option will be chosen over another. This can result in different settings making different binaries. And different compilers will likely use different default settings.
does the binary generated from compiling A language differs from the binary generated from compiling B language?
For the same reasons as above, unless the compiler settings are exactly the same, and the problem we ask the compiler to solve is exactly the same, we will likely have different binary outputs.
Edit:
In some cases, using a different language results in different assumptions around what you can and cannot do. For example, in Fortran, it is assumed that each pointer in a function is unique. This allows the compiler to optimize around this fact. (i.e. load things from RAM once and then save them in a cache). C does not have this assumption, so frequently data is reloaded from RAM if the compiler cannot determine that a pointer is unique. C99 introduced the restrict keyword to allow programmers to inform the compiler to treat a C pointer like a Fortran pointer.
I want to write cross-platform C/C++ which has reproducible behaviour across different environments.
I understand that gcc's ffast-math enables various floating-point approximations. This is fine, but I need two separately-compiled binaries to produce the same results.
Say I use gcc always, but variously for Windows, Linux, or whatever, and different compiler versions.
Is there any guarantee that these compilations will yield the same set of floating-point approximations for the same source code?
No, it's not that they allow specific approximations, it's that -ffast-math allows compilers to assume that FP math is associative when it's not. i.e. ignore rounding error when transforming code to allow more efficient asm.
Any minor differences in choice of order of operations can affect the result by introducing different rounding.
Older compiler versions might choose to implement sqrt(x) as x * approx_rsqrt(x) with a Newton-Raphson iteration for -ffast-math, because older CPUs had a slower sqrtps instruction so it was more often worth it to replace it with an approximation of the reciprocal-sqrt + 3 or 4 more multiply and add instructions. This is generally not the case in most code for recent CPUs, so even if you use the same tuning options (especially the default -mtune=generic instead of -mtune=haswell for example), the choices that option makes can change between GCC versions.
It's hard enough to get deterministic FP without -ffast-math; different libraries on different OSes have different implementations of functions like sin and log (which unlike the basic ops + - * / sqrt are not required to return a "correctly rounded" result, i.e. max error 0.5ulp).
And extra precision for temporaries (FLT_EVAL_METHOD) can change the results if you compile for 32-bit x86 with x87 FP math. (-mfpmath=387 is the default for -m32). If you want to have any hope here, you'll want to avoid 32-bit x86. Or if you're stuck with it, maybe you can get away with -msse2 -mfpmath=sse...
You mentioned Windows, so I'm assuming you're only talking about x86 GNU/Linux, even though Linux runs on many other ISAs.
But even just within x86, compiling with -march=haswell enables use of FMA instructions, and GCC defaults to #pragma STDC FP_CONTRACT ON (even across C statements, beyond what the usual ISO C rules allow.) So actually even without -ffast-math, FMA availability can remove rounding for the x*y temporary in x*y + z.
With -ffast-math:
One version of gcc might decide to unroll a loop by 2 (and use 2 separate accumulators), when summing sum an array, while an older version of gcc with the same options might still sum in order.
(Actually current gcc is terrible at this, when it does unroll (not by default) it often still uses the same (vector) accumulator so it doesn't hide FP latency the way clang does. e.g. https://godbolt.org/z/X6DTxK uses different registers for the same variable, but it's still just one accumulator, no vertical addition after the sum loop. But hopefully future gcc versions will be better. And differences between gcc versions in how they do a horizontal sum of a YMM or XMM register could introduce differences there when auto-vectorizing)
Why do people prefer LLVM IR, and how exactly is it different from the GCC IR? Is target dependency a factor here?
I'm a complete newbie to compilers, and wasn't able to find anything relevant even after many hours of searching for an answer. Any insights would be helpful.
Firstly, as this answer touches on complex and sensitive topics I want to make few disclaimers:
I assume your question is about middle-end IRs of LLVM and GCC (as the term "LLVM IR" applies only to middle-end). Discussion of differences of back-end IRs (LLVM MachineIR and GCC RTL) and related codegen tools (LLVM Tablegen and GCC Machine Description) is an interesting and important topic but would make the answer several times larger.
I left out library-based design of LLVM vs monolithic design of GCC as this is separate from IR per se (although related).
I enjoy hacking on both GCC and LLVM and I do not put one ahead of other. LLVM is what it is because people could learn from things that GCC had wrong back in 2000-s (and which have been significantly improved since then).
I'm happy to improve this answer so please post comments if you think that something is imprecise or missing.
The most important fact is that LLVM IR and GCC IR (called GIMPLE) are not that different in their core - both are standard control-flow graphs of basic blocks, each block being a linear sequence of 2 inputs, 1 output instructions (so called "three-address code") which have been converted to SSA form. Most production compilers have been using this design since 1990-s.
Main advantages of LLVM IR are that it's less tightly bound to compiler implementation, more formally defined and has nicer C++ API. This allows for easier processing, transformation and analysis, which makes it IR of choice these days, both for compiler and for other related tools.
I expand on benefits of LLVM IR in subchapters below.
Standalone IR
LLVM IR originally designed to be fully reusable across arbitrary tools besides compiler itself. The original intent was to use it for multi-stage optimization: IR would be consequently optimized by ahead-of-time compiler, link-time optimizer and JIT compiler at runtime. This didn't work out but reusability had other important implications, most noticeably it allowed easy integration of other types of tools (static analyzers, instrumenters, etc.).
GCC community never had desire to enable any tools besides compiler (Richard Stallman resisted attempts to make IR more reusable to prevent third-party commercial tools from reusing GCC's frontends). Thus GIMPLE (GCC's IR) was never considered to be more than an implementation detail, in particular it doesn't provide a full description of compiled program (e.g. it lacks program's call graph, type definitions, stack offsets and alias information).
Flexible pipeline
The idea of reusability and making IR a standalone entity led to an important design consequence in LLVM: compilation passes can be run in any order which prevents complex inter-pass dependencies (all dependencies have to be made explicit via analysis passes) and enables easier experimentation with compilation pipeline e.g.
running strict IR verification checks after each pass
bisecting pipeline to find a minimal subset of passes which cause compiler crash
fuzzing order of passes
Better unit-testing support
Standalone IR allows LLVM to use IR-level unit tests which allows easy testing of optimization/analysis corner-cases. This is much harder to achieve through C/C++ snippets (as in GCC testsuite) and even when you manage, the generated IR will most likely change significantly in future versions of the compiler and the corner case that your test was intended for will no longer be covered.
Simple link-time optimization
Standalone IR enables easy combination of IR from separate translation units with a follow-up (whole program) optimization. This is not a complete replacement for link-time optimization (as it does not deal with scalability issues which arise in production software) but is often good enough for smaller programs (e.g. in embedded development or research projects).
Stricter IR definition
Although criticized by academia, LLVM IR has a much stricter semantics compared to GIMPLE. This simplifies implementation of various static analyzers e.g. IR Verifier.
No intermediate IRs
LLVM IR is generated directly by the frontend (Clang, llgo, etc.) and preserved throughout the whole middle-end. This means that all tools, optimizations and internal APIs only need to operate on single IR. The same is not true for GCC - even GIMPLE has three distinct variants:
high GIMPLE (includes lexical scopes, high-level control-flow constructs, etc.)
pre-SSA low GIMPLE
final SSA GIMPLE
and also GCC frontends typically generate intermediate GENERIC IR instead of GIMPLE.
Simpler IR
Compared to GIMPLE, LLVM IR was deliberately made simpler by reducing number of cases which IR consumers need to consider. I've added several examples below.
Explicit control-flow
All basic blocks in LLVM IR program have to end with explicit control-flow opcode (branch, goto, etc.). Implicit control flow (i.e. fallthrough) is not allowed.
Explicit stack allocations
In LLVM IR virtual registers do not have memory. Stack allocations are represented by dedicated alloca operations. This simplifies working with stack variables e.g. equivalent of GCC's ADDR_EXPR is not needed.
Explicit indexing operations
Contrary to GIMPLE which has plethora of opcodes for memory references (INDIRECT_REF, MEM_REF, ARRAY_REF, COMPONENT_REF, etc.), LLVM IR has only plain load and store opcodes and all complex arithmetic is moved to dedicated structured indexing opcode, getelementptr.
Garbage collection support
LLVM IR provides dedicated pseudo-instructions for garbage-collected languages.
Higher-level implementation language
While C++ may not be the best programming language, it definitely allows to write much simpler (and in many case more functional) system code,
especially with post-C++11 changes (LLVM aggressively adopts new Standards). Following LLVM, GCC has also adopted C++ but majority of the codebase is still written in C style.
There are too many instances where C++ enables a simpler code so I'll just name a few.
Explicit hierarchy
The hierarchy of operators in LLVM is implemented via standard inheritance and template-based custom RTTI. On the other hand GCC achieves the same via old-style inheritance-via-aggregation
// Base class which all operators aggregate
struct GTY(()) tree_base {
ENUM_BITFIELD(tree_code) code : 16;
unsigned side_effects_flag : 1;
unsigned constant_flag : 1;
unsigned addressable_flag : 1;
... // Many more fields
};
// Typed operators add type to base data
struct GTY(()) tree_typed {
struct tree_base base;
tree type;
};
// Constants add integer value to typed node data
struct GTY(()) tree_int_cst {
struct tree_typed typed;
HOST_WIDE_INT val[1];
};
// Complex numbers add real and imaginary components to typed data
struct GTY(()) tree_complex {
struct tree_typed typed;
tree real;
tree imag;
};
// Many more operators follow
...
and tagged union paradigms:
union GTY ((ptr_alias (union lang_tree_node),
desc ("tree_node_structure (&%h)"), variable_size)) tree_node {
struct tree_base GTY ((tag ("TS_BASE"))) base;
struct tree_typed GTY ((tag ("TS_TYPED"))) typed;
struct tree_int_cst GTY ((tag ("TS_INT_CST"))) int_cst;
struct tree_complex GTY ((tag ("TS_COMPLEX"))) complex;
All GCC operator APIs use the base tree type which is accessed via fat macro interface (DECL_NAME, TREE_IMAGPART, etc.). Interface is only verified at runtime (and only if GCC was configured with --enable-checking) and does not allow static checking.
More concise APIs
LLVM generally provides simpler APIs for pattern matching IR in optimizers. For example checking that instruction is an addition with constant in GCC looks like
if (gimple_assign_p (stmt)
&& gimple_assign_rhs_code (stmt) == PLUS_EXPR
&& TREE_CODE (gimple_assign_rhs2 (stmt)) == INTEGER_CST)
{
...
and in LLVM:
if (auto BO = dyn_cast<BinaryOperator>(V))
if (BO->getOpcode() == Instruction::Add
&& isa<ConstantInt>(BO->getOperand(1))
{
Arbitrary-precision arithmetic
Due to C++ support for overloading, LLVM can uses arbitrary-precision ints for all computations whereas GCC still uses physical integers (HOST_WIDE_INT type, which is 32-bit on 32-bit hosts):
if (!tree_fits_shwi_p (arg1))
return false;
*exponent = tree_to_shwi (arg1);
As shown in the example this can lead to missed optimizations.
GCC has got an equivalent of APInts few years ago but the majority of the codebase still uses HOST_WIDE_INT.
our compilers course features exercises asking us to compare code built with the -O and -O3 gcc options. The code generated by my machine isn't the same as the code in the course. Is there a way to figure the optimization options used in the course, in order to obtain the same code on my machine, and make more meaningful observations?
I found how to get the optimization options on my machine :
$ gcc -O3 -Q --help=optimizer
But is there a way to deduce those on the machine of the professor except by trying them all and modifying them one by one (.ident "GCC: (Debian 4.3.2-1.1) 4.3.2")?
Thanks for your attention.
Edit:
I noticed that the code generated on my machine lacks the prologue and epilogue generated on my professor's. Is there an option to force prologue generation (google doesn't seem to bring much)?
Here's what you need to know about compiler optimizations : they are architecture dependent. Also, they're mainly different from one version of the compiler to another (gcc-4.9 does more stuff by default than gcc-4.4).
By architecture, I mean CPU micro architecture (Intel : Nehalem, Sandy bridge, Ivy Bridge, Haswell, KNC ... AMD : Bobcat, Bulldozzer, Jaguar, ...). Compilers usually convert input code (C, C++, ADA, ...) into a CPU-agnostic intermediary representation (GIMPLE for GCC) on which a large number of optimizations will be performed. After that, the compiler will generate a lower level representation closer to assembly. On the latter, architecture specific optimizations will be unrolled. Such optimizations include the choice of instructions with the lowest latencies, determining loop unroll factors depending on the loop size, the instruction cache size, and so on.
Since your generated code is different from the one you got in class, I suppose the underlying architectures must be different. In this case, even with the same compiler flags you won't be able to get the same assembly code (even with no optimizations you'll get different assembly codes).
For that, you should concentrate on comparing the optimized and non-optimized codes rather than trying to stick to what you were given in class. I even think that it's a great reverse engineering exercise to compare your optimized code to the one you were given.
You can find one of my earlier posts about compiler optimizations in here.
Two great books on the subject are The Dragon Book (Compilers: Principles, Techniques, and Tools) by Aho, Seti, and Ulman, and also Engineering a Compiler by Keith Cooper, and Linda Torczon.
I just realized that binary compilers convert source code to the binary of the destination platform. Kind of obvious... but if a compiler works such way, then how can the same compiler be used for different systems like x86, ARM, MIPS, etc?
Shouldn't they be supposed to "know" the machine-language of the hardware platform to be able to know how to build the binary? Does a compiler(like gcc) knows the machine language of every single platform that is supported?
How is that system possible, and how can a compiler be optimized for that many platforms at the same time?
Yes, they have to "know" the machine language for every single platform they support. This is a required to generate machine code. However, compilation is a multi-step process. Usually, the first steps of the compilation are common to most architectures.
Taken from wikipedia
Structure of a compiler
Compilers bridge source programs in high-level
languages with the underlying hardware.
A compiler requires
determining the correctness of the syntax of programs,
generating correct and efficient object code,
run-time organization, and
formatting output according to assembler and/or linker conventions.
A
compiler consists of three main parts: the frontend, the middle-end,
and the backend.
The front end
checks whether the program is correctly
written in terms of the programming language syntax and semantics.
Here legal and illegal programs are recognized. Errors are reported,
if any, in a useful way. Type checking is also performed by collecting
type information. The frontend then generates an intermediate
representation or IR of the source code for processing by the
middle-end.
The middle end
is where optimization takes place. Typical
transformations for optimization are removal of useless or unreachable
code, discovery and propagation of constant values, relocation of
computation to a less frequently executed place (e.g., out of a loop),
or specialization of computation based on the context. The middle-end
generates another IR for the following backend. Most optimization
efforts are focused on this part.
The back end
is responsible for translating the IR from the middle-end into assembly code. The target
instruction(s) are chosen for each IR instruction. Register allocation
assigns processor registers for the program variables where possible.
The backend utilizes the hardware by figuring out how to keep parallel
execution units busy, filling delay slots, and so on. Although most
algorithms for optimization are in NP, heuristic techniques are
well-developed.
More this article which describes the structure of a compiler and on this one which deals with Cross compilers.
The http://llvm.org/ project will answer all of your questions in this regard :)
In a nutshell, cross HW compilers emit "intermediate representation" of the code , which is HW agnostic and then its being customized via the native tool chain
Yes it is possible, it's called Cross Compiler. Compilers usually first they generate the object code which is not understanable by the current machine but it can be migrated to the destiny machine with another compiler. Next, object code is "compiled" again and linked with external libraries of the target machines.
TL;DR: Yes, the compilers knows the target code, but you can compile in another hardware.
I recommend you to read attached links for information.
Every platform has its own toolchain, toolchain includes gcc,gdb,ld,nm etc.
Let's take specific example of gcc as of now. GCC source code has many layers including architecture dependent and independent part. Architecture dependent part contains procedures to handle architecture specific things like their stack, function calls, floating point operations. We need to cross compile the gcc source code for a specific architecture like for ARM. You can see its steps here for reference:- http://www.ailis.de/~k/archives/19-arm-cross-compiling-howto.html#toolchain.
This architecture dependent part is responsible for handling machine language operations.