Domanda

New Preface

I'm not that knowledgeable on constructor theory, and this is a theoretical question on how a theoretical constructor for a C++ like language and how it would compile very similar methods to assembler (and further into binary code), and whether that assembler would be similar across three separate methods which each take the same number of arguments but provide the same functionality (perhaps a trivial method that prints two integer values?).

Each of the methods takes a pair of integers as arguments but in different ways: one method takes pass by value, another by reference and a final one via address. Since the pass by reference and address variants would have to act on the actual value of the memory location supplied, would they (aside from the code to access the values) contain the same code in the compiled version?

public void Foo (int a, int b)
{
   std:cout << a << " " << b <<endl;
}

public void Bar (int* a, int* b)
{
  // (aside from dereferencing code) the same code as Foo
}

public void FooBar (int& a, int& b)
{
  // again, the same code (fundamentally) here
}

Original Question

Suppose I have written three identical methods "Foo", "Bar" and "FooBar". Each of the methods take in the same finite number of arguments, and all methods are void.

public void Foo (int a, int b)
{
  // some code here
}

public void Bar (int* a, int* b)
{
  // (aside from dereferencing code) the same code as Foo
}

public void FooBar (int& a, int& b)
{
  // again, the same code (fundamentally) here
}

Suppose that the compiler for this language is working perfectly with no bugs and full optimisation.

In a perfect such a system, would the outputted assembly and binary code for all three methods not match? If not, would there be a huge amount of difference between them?

Clarification: Assume these methods are extremely trivial in that they do not alter the values of the passed in arguments.

È stato utile?

Soluzione

Obviously, if the code does modify a and b the semantic of the first and the last two functions is entirely different; that would obviously force the compiler to generate different code; besides this, if the three functions actually acted the same in every circumstance in a given program, then of course the compiler is free to reduce all them to a single function thanks to the "as if" rule.

But then this question wouldn't make much sense - or at least, asking about specific differences in binary code generation (a very specific implementation detail) in a "perfect system" (something that cannot exist) to me isn't really meaningful.

To get back to real systems, first of all we must consider inlining; if the functions are inlined, then they get mixed with the rest of the code of the parent function and the output may be different in each expansion (there can be different register allocation, different instruction intermixing to maximize pipeline utilization, ...)

So, the comparison should be about "standalone" output for each function.

I would expect the second and the third one to expand to almost the same exact same code: references are syntactic sugar for pointers, and the fact that you cannot have a NULL reference is already taken into account by the fact that *a and *b are probably dereferenced without checks in Bar (which tells the optimizer to assume that they won't ever be NULL). Also, I don't know of any C++ ABI that differentiates pointers from references.

As for Foo, it would depend on many factors:

  • if we are compiling a library, the compiler isn't free to do whatever it wants, and functions must adhere to some ABI; for this reason, first of all the parameters would actually be pointers in the last two cases and values in the first one (with varying consequences depending from the platform ABI);
  • this can hold also if the compiler isn't capable of LTCG and we are expecting to use these functions from other modules (i.e. the functions aren't marked as static);
  • in the last two cases, to generate the same output the compiler may be required to prove that the references/pointers point to different values to generate the same output as for Foo;
  • also, it must be able to prove that a and b aren't changed throughout the function; in particular, after each external (=non-completely-inlined) function call the pointed objects may have changed; both of these can be complicated tasks, and, again, they may require LTCG if the program is made of several modules.

So what I actually expect to happen is:

  • for standalone versions, Foo!= Bar and FooBar; Bar==FooBar;
  • as for inlined versions, the compiler will probably have a simpler time to determine if the conditions for "converting" Bar and FooBar to the same semantic of Foo, but of course the fact that the generated code is intermixed with code of a different function will result in different assembly output (to the point that it may be difficult to understand where starts/ends the code of the subroutine).

Altri suggerimenti

An optimizing compiler will very likely produce better code for Foo than for Bar or FooBar, except for rather trivial functions.

The reason is that for Foo, the compiler may assume that the values of a and b are constant throughout the function, unless there's an explicit assignment to one of these variables. And even if there is, most modern compiler's intermediate representation will represent such an assignment as a new variable, and simply use that variable instead of the original one from the point of assignment onward.

For Bar and FooBar, however, that kind of reasoning only works if

  • The function doesn't call other non-inlined functions, since any such function could change the values pointed to by a or b

  • The function doesn't modify any memory through a pointer of type char*, since such pointer may legally point to data of any type, including int (Google for "strict aliasing rules" for the full scoop)

Bar and FooBar will likely produce similar code, unless you're on some arcane platform where the ABI treans references and pointers differently.

In a prefect system, yes..!!

However, this level of optimization is quite extreme. Bar and FooBar are almost the same as Foo, but the fact they're almost the same would make it difficult for the compiler to detect the similarities, as it's basically doing a diff on the generated code and then having to work out how significant the differences are.

If you want this level of optimizing then you might be better off writing the code like this

public void Foo(int a, int b)
{
  // Whatever
}

public void Bar (int* a, int* b)
{
  Foo(*a, *b);
}

public void FooBar (int& a, int& b)
{
  Foo(a, b);
}

Now the compiler may choose to inline Foo into Bar and FooBar, which is a fairly straightforward optimization decision for a compiler to make.

Assuming C++ style declarations, Bar and Foobar will be identical except the debug information, because references internally are pointers, and only access style distnguishes them from pointers.

With the first one (Foo), it depends whether you really allow to modify the function declaration style if it isn't known how the functions will be used in future. If their interface supposes they can change values under pointers (even if under quite extreme conditions), optimization with passing only values isn't allowed. But, assuming very often call of the function, compiler and/or runtime can change it to a variant without pointers, with direct value passing. (But, more likely, some other optimization will be applied, e.g. full function inlining.)

In a perfect system, would the outputted assembly and binary code for all three methods not match?

No because if the code writes to a and b, then Foo modifies a local copy of a and b, whereas FooBar modifies the value of the original (caller's copy of) a and b.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top