Parameter value optimization?

Started by
17 comments, last by ToohrVyk 15 years, 9 months ago
This thread got me thinking. Suppose I do this:
struct Obj {
    int a;
    char b;

    Obj(int a, char b):a(a), b(b) {}
};

std::vector<Obj> vec;
Obj obj(5, 'b');
vec.push_back(obj);
In this code, first Obj's constructor is called, then it's copy constructor is called to copy it into the vector, then it's destructor is called. But suppose that I have no need for obj. I only want to create an object "inside" the vector, so to speak. If it was possible to create the object directly into the vector's memory, then the copy constructor and destructor would not need to be called. This situation kind of reminds me of the return value optimization, so my question is: is there something like this with parameters? That is, if I change the above code to this: vec.push_back(Obj(5, 'b')); would the compiler be able to optimize the temporary away? Also, I've heard that there's also the named return optimization, where the compiler can optimize away the temporary even if it's created as an object with a name (i.e., not just in the return statement). Can the compiler do something like this with the first code snippet? Thanks in advance.
Advertisement
In theory, yes. You could write code such as:

void foo(Bar a);int main() { foo(Bar()); }


And no temporary value would be created, skipping the copy construction and instantiating the argument directly in the function.

In practice, what happens is that the push-back function usually looks like:

void push_back(const value_type &t){  if (size() == capacity()) { reserve(capacity() * 2); }  new (this->_data + size()) value_type(t);  ++ this->_size;}


Meaning the calling function cannot know where the object should be instantiated, and thus no optimization can be applied. Of course, some compilers may apply some additional optimizations to their associated SC++L implementations, but I don't think this one happens.
It's tricky. Very tricky. Mostly because for your very simple example, it may behave differently than a *real* example.

Compilers are actually surprisingly stupid when you really confront them with something that is semantically obvious, but syntactically complicated, like what you are putting forward here. It is clear what your intent is. However, with all programming languages, there isn't always a direct mapping between what a programmer means, and what a programmer says [thus, logically different code that is *meant* to be the same]. For example, what you have here.

Consider this:
Obj c;c = Obj(a,b);
vs:
Obj c(a,b)
or even:
Obj c = Obj(a,b)
What you MEAN, is clear. You mean for these all to be the same. What you SAY, isn't the same. And the compiler can only mess with what you say. The first case resolves into a fully completing empty constructor followed by an assingment. The second is the (int,char) constructor, and it turns out that the third case is also the (int,char) constructor [though in a rather strange form].

So.... The std::vector<T>::pushback(T&) function will most certainly be inlined. In all likelihood, so will your constructors and your assignment in this simple case. And once it is all inlined, the compiler will see these redundant assignments of variables, and hack them up. It will optimize the code, but it will likely only actually end up being expressed as you are describing in the most simplistic of cases. Cases where everything gets inlined, and everything gets chewed up, and everything means the same thing [which turns out very frequently to be semantically equivalent, but not structurally equivalent]. It turns out that this isn't going to be a specific optimization with its own fancy name [at least not a name I'm familiar with], but instead an effect of function inlining and an a few expression simplification tricks.

But again, as always, it depends heavily on the individual compiler.

And yes, a compiler can optimize away a return value pretty easily in many cases.

*EDIT* Toohrvyk brings up a good point about the complications brought up by using the vector class. On a side note though, it doesn't have to know exactly where the data will go to grind on the assignments and simplify the equations if everything has been inlined successfully.
I think this can be solved if you could do something like this:

void push_back(arguments){  if (size() == capacity()) { reserve(capacity() * 2); }  new (this->_data + size()) value_type(arguments);  ++ this->_size;}


Then this:

vec.push_back(Obj(5, 'b'));

would become this:

vec.push_back(5, 'b');

That is, if push_back could accept a variable number of arguments, then it could construct the object directly in memory, and then there would be no need to create a temporary object.

I think this is what's being requested in the other thread that I linked. I guess doing this in C++ is very complicated, if it's even possible.
Isn't C++0x going to solve this issue with move semantics? Hooray, another meaning for the double-ampersand :)
Quote:Original post by Gage64
I think this can be solved if you could do something like this:


There are hundreds of ways this could be solved, with small patches to the language. But ultimately, it's the entire language philosophy and semantics that cause the issue: the C++ timidly supports the distinction between initialized and uninitialized memory, meaning it's good enough to cause problems but not good enough to solve them elegantly.
I've been looking around a bit and it looks like boost::object_pool allows something like this using construct(). Maybe it's possible to modify std::vector to allow this as well? Would it be worth doing?
btw, sometimes, to avoid the temporary you can just grow your vector first and take a reference on the last element. The resize will internally call the default constructor of T though.

std::vector<T> myVector;...size_t size = myVector.size();myVector.resize( size + 1 );     // default constructor calledT& myElement = myVector[size];   // initialize the element afterwards
Quote:Original post by fboivin
The resize will internally call the default constructor of T though.
Which is exactly why it doesn't help the situation any. You are also breaking encapsulation in doing this, and requiring that your objects are designed with entirely public fields for this to have any performance impact at all. In short, you're trading one performance problem that is likely rather mild, for a design problem that can be more significant.
Quote:Original post by ToohrVyk
There are hundreds of ways this could be solved, with small patches to the language. But ultimately, it's the entire language philosophy and semantics that cause the issue: the C++ timidly supports the distinction between initialized and uninitialized memory, meaning it's good enough to cause problems but not good enough to solve them elegantly
I'm not going to go into it too much for fear of starting some sort of language war, but you bring up a good point about C++ in particular. This issue really is only the tip of the iceberg of reasons that compiler writers hate C and C++. There are so many little details of the C and C++ specifications that make certain compile time optimization intensely difficult or even completely impossible that are rather simple in other languages. Couple this with adoption of common practices that are not actually part of the standard resulting in code that compiles fine if optimizations that rely on standards compliance are all turned off, and breaks horribly if optimizations are turned on, and you have a monster of a mess on your hands.

[Edited by - Drigovas on July 8, 2008 6:29:29 AM]
Quote:Original post by fboivin
btw, sometimes, to avoid the temporary you can just grow your vector first and take a reference on the last element. The resize will internally call the default constructor of T though.

*** Source Snippet Removed ***


No, in that case also unnecessary assignment operator call will be made, if the compiler is not "smart enough" to optimize it. In perfect case we should be able to construct the element *directly* in the vector.

This topic is closed to new replies.

Advertisement