245

I've always thought it's the general wisdom that std::vector is "implemented as an array," blah blah blah. Today I went down and tested it, and it seems to be not so:

Here's some test results:

UseArray completed in 2.619 seconds
UseVector completed in 9.284 seconds
UseVectorPushBack completed in 14.669 seconds
The whole thing completed in 26.591 seconds

That's about 3 - 4 times slower! Doesn't really justify for the "vector may be slower for a few nanosecs" comments.

And the code I used:

#include <cstdlib>
#include <vector>

#include <iostream>
#include <string>

#include <boost/date_time/posix_time/ptime.hpp>
#include <boost/date_time/microsec_time_clock.hpp>

class TestTimer
{
    public:
        TestTimer(const std::string & name) : name(name),
            start(boost::date_time::microsec_clock<boost::posix_time::ptime>::local_time())
        {
        }

        ~TestTimer()
        {
            using namespace std;
            using namespace boost;

            posix_time::ptime now(date_time::microsec_clock<posix_time::ptime>::local_time());
            posix_time::time_duration d = now - start;

            cout << name << " completed in " << d.total_milliseconds() / 1000.0 <<
                " seconds" << endl;
        }

    private:
        std::string name;
        boost::posix_time::ptime start;
};

struct Pixel
{
    Pixel()
    {
    }

    Pixel(unsigned char r, unsigned char g, unsigned char b) : r(r), g(g), b(b)
    {
    }

    unsigned char r, g, b;
};

void UseVector()
{
    TestTimer t("UseVector");

    for(int i = 0; i < 1000; ++i)
    {
        int dimension = 999;

        std::vector<Pixel> pixels;
        pixels.resize(dimension * dimension);

        for(int i = 0; i < dimension * dimension; ++i)
        {
            pixels[i].r = 255;
            pixels[i].g = 0;
            pixels[i].b = 0;
        }
    }
}

void UseVectorPushBack()
{
    TestTimer t("UseVectorPushBack");

    for(int i = 0; i < 1000; ++i)
    {
        int dimension = 999;

        std::vector<Pixel> pixels;
            pixels.reserve(dimension * dimension);

        for(int i = 0; i < dimension * dimension; ++i)
            pixels.push_back(Pixel(255, 0, 0));
    }
}

void UseArray()
{
    TestTimer t("UseArray");

    for(int i = 0; i < 1000; ++i)
    {
        int dimension = 999;

        Pixel * pixels = (Pixel *)malloc(sizeof(Pixel) * dimension * dimension);

        for(int i = 0 ; i < dimension * dimension; ++i)
        {
            pixels[i].r = 255;
            pixels[i].g = 0;
            pixels[i].b = 0;
        }

        free(pixels);
    }
}

int main()
{
    TestTimer t1("The whole thing");

    UseArray();
    UseVector();
    UseVectorPushBack();

    return 0;
}

Am I doing it wrong or something? Or have I just busted this performance myth?

I'm using Release mode in Visual Studio 2005.


In Visual C++, #define _SECURE_SCL 0 reduces UseVector by half (bringing it down to 4 seconds). This is really huge, IMO.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
kizzx2
  • 18,775
  • 14
  • 76
  • 83
  • what compiler flags do you use? generally STL relies on compiler optimization for performance – Anycorn Sep 08 '10 at 02:41
  • 26
    Some version of vector when you are in debug mode add extra instructions to check that you don't access beyond the end of array and stuff like that. To get real timings you must build in release mode and turn the optimizations on. – Martin York Sep 08 '10 at 02:48
  • 3
    Are you using Debug mode or Release mode? I agree that the performance of `std::vector` in Debug mode may cause significant concerns if it is heavily used. – rwong Sep 08 '10 at 02:49
  • 49
    It's good that you have measured instead of believing claims you heard over the Internet. – P Shved Sep 08 '10 at 04:46
  • With VC++, try compiling from command line using `/O2`. I'm not sure what exactly you need to set in project properties to get the same effect, but using `/O2` rather than `/Os` with VC++2010 gives me UseVectorPushBack that is only 60% slower than UseArray, rather than being twice as slow. – Pavel Minaev Sep 08 '10 at 05:09
  • 53
    vector *is* implemented as an array. That's not "conventional wisdom", its the truth. You've discovered that `vector` is a general purpose resizable array. Congratulations. As with all general purpose tools, it is possible to come up with specialized situations where it is sub-optimal. Which is why the conventional wisdom is to *start* with a `vector` and consider alternatives if neccessary. – Dennis Zickefoose Sep 08 '10 at 05:18
  • 37
    lol, whats the speed difference of "throwing dirty dishes into a sink" and "throwing dirty dishes into a sink and checking if they didn't break" ? – Imre L Sep 08 '10 at 07:18
  • 9
    On VC2010 at least it seems the major difference is that malloc() is faster than resize(). Remove memory allocation from the timing, compile with _ITERATOR_DEBUG_LEVEL == 0 and the results are the same. – Andreas Magnusson Sep 08 '10 at 11:21
  • 5
    You can answer any question of this sort by single-stepping in the disassembly window. I'm amazed at how many folks would rather throw guesses around than just "open the box" and see what's going on. – Mike Dunlavey Sep 08 '10 at 12:19
  • 5
    @Mike: Obviously not many of us young folks around here are as comfortable with assembly as wizards do. Care to write an answer on how to dissect this problem using the real man's way? (assembly)? – kizzx2 Sep 08 '10 at 15:15
  • 4
    @Mike: Just re-read my comment above and thought I'd phrase it a little better so my meaning doesn't get misunderstood over the wire. "I'd really love a demonstration with some explanation on how to approach the problem from a disassembling angle. I know how to run a disassembler but can't dissect this problem from start to finish. In particular, if I turn on optimizations, I get pretty much hairy assembly. If I disabled optimizations, people could argue STL is optimized for optimizations turned on." – kizzx2 Sep 08 '10 at 15:27
  • 1
    @kizzx2: No need to be a wizard to follow assembly language. First, turn off the optimization, 'cause it'll just scramble the code. Then step along at the instruction level (displaying the source code as you go). It's pretty simple: move something to a register, multiply it, use it as an address to fetch something, compare things, conditionally jump, call a function, etc. If you show the source line as you go, pretty soon you'll understand how the compiler thinks, and you'll know exactly what's going on. If it gets too lengthy, skip over some parts. – Mike Dunlavey Sep 08 '10 at 17:01
  • 1
    Reading assembly is pretty straightforward. Download the manuals from Intel or AMD, and look up each new instruction you encounter. Writing assembly is the tricky part, but as @Mike said, asm is a very simple "language", and it's straightforward to read (it can be tedious and take time, but it's not difficult) – jalf Sep 12 '10 at 01:46
  • 5
    @Mike: Note that turning off optimization can screw up timing. Some implementations of vector use a range-checked version when not being optimized, and a faster version when optimized. Stepping through as you suggest will show that it's using the slower but safer version, while time-testing at high optimization will show the opposite. – David Thornley Sep 21 '10 at 15:13
  • 1
    @David: You're right, it affects code at the micro level, but you can still see that that's what it's doing. Then if you need to turn on optimization you can also see what it's doing then. When I have to step through Fortran in assembler, I find it almost impossible to follow if it's been optimized. Nevertheless, I think it's a useful skill. – Mike Dunlavey Sep 21 '10 at 16:25
  • 3
    Almost a duplicate of *[Using arrays or std::vectors in C++, what's the performance gap?](http://stackoverflow.com/questions/381621/using-arrays-or-stdvectors-in-c-whats-the-performance-gap)* –  Oct 19 '10 at 08:25
  • @Roger Pate: this is unfortunate. I don't know what makes you think this is "almost a duplicate" but if you've read through the code and the posts you'll find that it's actually talking about different things. What's the purpose of the drive by? (That post's accepted answer focuses on the indexing and the dereferencing. This thread's ultimate answer lies in the constructor) – kizzx2 Oct 19 '10 at 15:05
  • 3
    Drive by? I was just pointing out a closely related question. –  Oct 19 '10 at 15:07
  • 2
    I'd be interested to see how this turned out if you called `memset(pixels, 0, sizeof(Pixel) * dimension * dimension)` after your call to `malloc()` in `UseArray()`. – Drew Chapin Dec 03 '11 at 06:51
  • It would be interesting to see the same thing with std:Array<> as well! I haven't read all the answers, so perhaps it's been tested already ;) – AzP Sep 17 '12 at 09:53
  • allocate your vector with std::vector pixels(dimension*dimension), this will remove the extra resize overhead compared to the array – galinette Apr 03 '14 at 20:14

22 Answers22

279

Using the following:

g++ -O3 Time.cpp -I <MyBoost>
./a.out
UseArray completed in 2.196 seconds
UseVector completed in 4.412 seconds
UseVectorPushBack completed in 8.017 seconds
The whole thing completed in 14.626 seconds

So array is twice as quick as vector.

But after looking at the code in more detail this is expected; as you run across the vector twice and the array only once. Note: when you resize() the vector you are not only allocating the memory but also running through the vector and calling the constructor on each member.

Re-Arranging the code slightly so that the vector only initializes each object once:

 std::vector<Pixel>  pixels(dimensions * dimensions, Pixel(255,0,0));

Now doing the same timing again:

g++ -O3 Time.cpp -I <MyBoost>
./a.out
UseVector completed in 2.216 seconds

The vector now performance only slightly worse than the array. IMO this difference is insignificant and could be caused by a whole bunch of things not associated with the test.

I would also take into account that you are not correctly initializing/Destroying the Pixel object in the UseArrray() method as neither constructor/destructor is not called (this may not be an issue for this simple class but anything slightly more complex (ie with pointers or members with pointers) will cause problems.

chema989
  • 3,962
  • 2
  • 20
  • 33
Martin York
  • 257,169
  • 86
  • 333
  • 562
  • I was aware of the fact that I waste initializations with `resize()` -- that's why I added the `push_back` version (which doesn't do "wasteful" initializations but turned out to be slower). I'm not aware of how you can simply get a vector of uninitialized bunch of memory like `malloc` – kizzx2 Sep 08 '10 at 03:00
  • 53
    @kizzx2: You need to use `reserve()` instead of `resize()`. This allocates space for the objects (that is, it changes the _capacity_ of the vector) but does not create the objects (that is, the _size_ of the vector is left unchanged). – James McNellis Sep 08 '10 at 03:03
  • 1
    Supplying your own allocator as a template argument may also help in getting vector to just grab its memory in big uninitialized heap chunks. – Crashworks Sep 08 '10 at 03:04
  • 1
    @James: That's what I tried to do in `UseVectorPushBack`. Turned out to be even slower. – kizzx2 Sep 08 '10 at 03:07
  • @Crashworks: I tried to look at it. Seems a total overkill for a simple scenario. Is there a built in `null_allocator` or something? – kizzx2 Sep 08 '10 at 03:09
  • So I guess `vector` would be "almost" as fast as arrays for long living objects. For short living objects, use arrays to eliminate the cost of initialization (unless you wanna write your own STL allocator) – kizzx2 Sep 08 '10 at 03:15
  • Honestly, kizzx2, in the realtime graphics world we just don't use the STL much in performance sensitive code. I've never been able to get the code gen to be identical to a simple array (so that advancing and dereferencing an iterator works out to exactly one add op and one load op). – Crashworks Sep 08 '10 at 03:20
  • 30
    You are doing 1 000 000 000 array accesses. The time difference is 0.333 seconds. Or a difference of 0.000000000333 per array access. Assuming a 2.33 GHz Processor like mine that's 0.7 instruction pipeline stages per array accesses. So the vector looks like it is using one extra instruction per accesses. – Martin York Sep 08 '10 at 03:25
  • Giving g++ the `-S` command line option will let you see exactly the assembly it is emitting. – Crashworks Sep 08 '10 at 03:30
  • 1
    Calling `Pixel()` in a loop is not entirely fair. Change the malloc + loop into `new Pixel[dimension * dimension]` and now the array loop is back to its previous fast speed. – John Kugelman Sep 08 '10 at 03:34
  • The final result looks more in line with popular opinions. But I wouldn't say "done correctly vector is faster," though. By 0.025 seconds :P And that's at the price of wasting the default constructor of a contained object *for the sake of the container*, or writing an STL allocator. – kizzx2 Sep 08 '10 at 03:45
  • 1
    @Martin "You are doing 1 000 000 000 array accesses. The time difference is 0.333 seconds. Or a difference of 0.000000000333 per array access. Assuming a 2.33 GHz Processor like mine that's 0.7 instruction pipeline stages per array accesses. So the vector looks like it is using one extra instruction per accesses." You sound like you want to exaggerate the numbers, but I guess looping over a bunch of 1000 * 1000 pixmap is a very common operation in any graphic program. – kizzx2 Sep 08 '10 at 03:47
  • 1
    It is, but we never do it in a linear componentwise way like this -- it's almost always done in SIMD, either as a shader program or as an unrolled loop of intrinsics on the CPU, processing multiple pixels at a time. At this level of performance, even the pipeline bubble introduced by the loop branch is significant. (In fact I'm at work trying to fix some branch mispredicts in our renderer right now.) – Crashworks Sep 08 '10 at 03:49
  • 3
    @James McNellis: You can't just replace `resize()` with `reserve()`, because this does not adjust the vector's internal idea of its own size, so subsequent writes to its elements are technically "writing past the end" and will produce UB. Although in practice every STL implementation will "behave itself" in that regard, how do you resync the vector's size? If you try calling `resize()` *after* populating the vector, it will quite possibly overwrite all those elements with default-constructed values! – j_random_hacker Sep 08 '10 at 04:08
  • @kizzx2: Its 1,000,000,000 array accesses as you are doing an array of 1000*1000 elements AND then you have another loop (inside the function) that is repeating the operation 1000 times. – Martin York Sep 08 '10 at 04:12
  • 9
    @j_random_hacker: Isn't that exactly what I said? I thought I was very clear that `reserve` only changes the capacity of a vector, not its size. – James McNellis Sep 08 '10 at 04:16
  • 1
    @James: My point is that using `reserve()` instead of `replace()` as you suggest is useless, *because* the former does not change the size -- that's a crucial piece of information about the vector! Choosing to go this route implies that the program can't trust the value of `pixels.size()` afterwards. – j_random_hacker Sep 08 '10 at 04:46
  • What's really interesting is that VC++ ended up being slower with `assign()` than it is with `reserve()`+`push_back()`. Now that's just WTF. – Pavel Minaev Sep 08 '10 at 05:12
  • 2
    ... and ditto for using the assign-like constructor. Another WTF. Poking in disassembly now, but I think this is already bug-filing-worthy. I'll forward this to Stephan tomorrow. – Pavel Minaev Sep 08 '10 at 05:14
  • 7
    Okay, go figure. There was a lot of exception-related cruft in vector methods. Adding `/EHsc` to compilation switches cleaned that up, and `assign()` actually beats array now. Yay. – Pavel Minaev Sep 08 '10 at 05:18
  • @Pavel: Interesting, I hadn't considered the effects of exception-handlnig overheads. I do all my VC++ compilation with `/EHsc`. – j_random_hacker Sep 08 '10 at 05:23
  • 1
    Also mote that the cost of exceptions is practically undetectable when exceptions are not actually thrown and when they are effeciency us no longer your major concern. – Martin York Sep 08 '10 at 06:32
  • 3
    Simply compiling with exceptions enabled induces a measurable performance overhead compared to compiling with them removed in MSVC, regardless of whether you throw them or not. Try it. – Crashworks Sep 08 '10 at 07:13
  • 1
    @Crashworks: It is very true for 32-bit Windows, but much less for 64-bit. – Nemanja Trifunovic Sep 08 '10 at 13:04
  • @Crashworks: Not that I don;t believe you but maybe you should ask a question as to why that is (just like this question asked why are vectors slower and was proved wrong). I don't know if you are wright or wrong not having done the tests but from all the articles I have read they indicate the difference from using exceptions is not measurable. – Martin York Sep 08 '10 at 17:51
  • 1
    Can't find it right now, but somewhere on SO there's a question about overheads for exception handling when no exceptions are thrown where I argued that there must be nonzero overhead, since disassemblies show MSVC++ adds some unless it can prove no exceptions can occur. But it turned out that g++'s overhead genuinely was zero for the examples I tried -- quite an engineering feat I think! (Zero execution time overhead that is.) – j_random_hacker Sep 08 '10 at 18:33
  • 3
    I don't think that `for(...) { pixels[i] = Pixel(); }` is quite the same as `pixels.resize(...)`. The former makes N default constructor calls and N assignment operator calls, and the latter makes 1 default constructor call and N copy constructor calls. Not that it really matters from a functional standpoint, considering that both versions initialize the pixels to uninitialized garbage. But it does point out a difference from `new Pixel[...]`, which calls the default constructor N times and never uses the copy constructor or assignment operator to copy uninitialized garbage around. – bk1e Sep 09 '10 at 05:39
  • @bk1e: Interesting, didn't realise thought `resize()` took a (defaultable) 2nd parameter! I expect they'll all compile to almost identical machine code if optimisations are turned on, because of the "as-if" rule, but yes they're technically distinct operations. – j_random_hacker Sep 09 '10 at 10:30
  • The array case is faster because GCC vectorizes accesses, but somehow is unable to do so when dealing with vector. See my answer below. –  Oct 20 '12 at 19:55
  • 1
    @jons34yp: we showed conclusively that array access is **NOT** faster than vector. They have exactly the same characteristics. – Martin York Oct 21 '12 at 01:03
  • I've ran this example in visual studio 2013 and got similar results in release build. Vector being slower by at most 10%. Then I ran the exe outside of visual studio and the speed of useArray() doubled! While the vector speed only by about 30%. So in the end the useArray is still more then twice faster. – rozina Nov 17 '13 at 00:09
  • 1
    @rozina: Then you are a very unique person (or have a some unique set up). When I have done this kind of test on many systems over the years. The runtime cost of vector over a straight arrays is nothing. – Martin York Nov 17 '13 at 21:27
  • 1
    You should test it with `-DNDEBUG` too. – Lightness Races in Orbit Jun 22 '14 at 11:37
60

Great question. I came in here expecting to find some simple fix that would speed the vector tests right up. That didn't work out quite like I expected!

Optimization helps, but it's not enough. With optimization on I'm still seeing a 2X performance difference between UseArray and UseVector. Interestingly, UseVector was significantly slower than UseVectorPushBack without optimization.

# g++ -Wall -Wextra -pedantic -o vector vector.cpp
# ./vector
UseArray completed in 20.68 seconds
UseVector completed in 120.509 seconds
UseVectorPushBack completed in 37.654 seconds
The whole thing completed in 178.845 seconds
# g++ -Wall -Wextra -pedantic -O3 -o vector vector.cpp
# ./vector
UseArray completed in 3.09 seconds
UseVector completed in 6.09 seconds
UseVectorPushBack completed in 9.847 seconds
The whole thing completed in 19.028 seconds

Idea #1 - Use new[] instead of malloc

I tried changing malloc() to new[] in UseArray so the objects would get constructed. And changing from individual field assignment to assigning a Pixel instance. Oh, and renaming the inner loop variable to j.

void UseArray()
{
    TestTimer t("UseArray");

    for(int i = 0; i < 1000; ++i)
    {   
        int dimension = 999;

        // Same speed as malloc().
        Pixel * pixels = new Pixel[dimension * dimension];

        for(int j = 0 ; j < dimension * dimension; ++j)
            pixels[j] = Pixel(255, 0, 0);

        delete[] pixels;
    }
}

Surprisingly (to me), none of those changes made any difference whatsoever. Not even the change to new[] which will default construct all of the Pixels. It seems that gcc can optimize out the default constructor calls when using new[], but not when using vector.

Idea #2 - Remove repeated operator[] calls

I also attempted to get rid of the triple operator[] lookup and cache the reference to pixels[j]. That actually slowed UseVector down! Oops.

for(int j = 0; j < dimension * dimension; ++j)
{
    // Slower than accessing pixels[j] three times.
    Pixel &pixel = pixels[j];
    pixel.r = 255;
    pixel.g = 0;
    pixel.b = 0;
}

# ./vector 
UseArray completed in 3.226 seconds
UseVector completed in 7.54 seconds
UseVectorPushBack completed in 9.859 seconds
The whole thing completed in 20.626 seconds

Idea #3 - Remove constructors

What about removing the constructors entirely? Then perhaps gcc can optimize out the construction of all of the objects when the vectors are created. What happens if we change Pixel to:

struct Pixel
{
    unsigned char r, g, b;
};

Result: about 10% faster. Still slower than an array. Hm.

# ./vector 
UseArray completed in 3.239 seconds
UseVector completed in 5.567 seconds

Idea #4 - Use iterator instead of loop index

How about using a vector<Pixel>::iterator instead of a loop index?

for (std::vector<Pixel>::iterator j = pixels.begin(); j != pixels.end(); ++j)
{
    j->r = 255;
    j->g = 0;
    j->b = 0;
}

Result:

# ./vector 
UseArray completed in 3.264 seconds
UseVector completed in 5.443 seconds

Nope, no different. At least it's not slower. I thought this would have performance similar to #2 where I used a Pixel& reference.

Conclusion

Even if some smart cookie figures out how to make the vector loop as fast as the array one, this does not speak well of the default behavior of std::vector. So much for the compiler being smart enough to optimize out all the C++ness and make STL containers as fast as raw arrays.

The bottom line is that the compiler is unable to optimize away the no-op default constructor calls when using std::vector. If you use plain new[] it optimizes them away just fine. But not with std::vector. Even if you can rewrite your code to eliminate the constructor calls that flies in face of the mantra around here: "The compiler is smarter than you. The STL is just as fast as plain C. Don't worry about it."

John Kugelman
  • 349,597
  • 67
  • 533
  • 578
  • 2
    Again, thanks for actually running the code. It's sometimes easy to get bashed without reasons when someone attempts to challenge popular opinions. – kizzx2 Sep 08 '10 at 03:02
  • 3
    "So much for the compiler being smart enough to optimize out all the C++ness and make STL containers as fast as raw arrays." Nice comments. I have a theory that this "compiler is smart" is just a myth -- C++ parsing is extremely hard and the compiler is just a machine. – kizzx2 Sep 08 '10 at 03:18
  • You can see @Martin's answer. I think my test is a little synthesized and stressed the initialization part. For long-living arrays that aren't performance surgery-kind critical, `vector` probably justify it for the extra benefits it brings. – kizzx2 Sep 08 '10 at 03:20
  • 3
    I dunno. Sure, he was able to *slow down* the array test, but he didn't *speed up* the vector one. I edited in above where I removed the constructors from Pixel and made it a simple struct, and it was still slow. That's bad news for anyone who uses simple types like `vector`. – John Kugelman Sep 08 '10 at 03:24
  • 2
    I wish I really could upvote your answer twice. Smart ideas to try out (even though none really worked) that I couldn't even think of! – kizzx2 Sep 08 '10 at 03:31
  • As for idea #2, it's not surprising. I did some hard-core optimization before and for primitive types like `unsigned char`, it's faster for the CPU and the optimizer to juggle that in the register than in memory. If you used a reference, sometimes things are forced to go through a memory address. – kizzx2 Sep 08 '10 at 03:32
  • I don't think you actually removed the constructor in your test. Doesn't the compiler generated default constructor do a default construction on each member? You'd have to define a default constructor that explicitly does nothing to make a valid test. – Mark Ransom Sep 08 '10 at 04:12
  • @Mark kizzx2's original code has a do-nothing default constructor. When I removed it there was a 10% speedup. The compiler generated default constructor will default construct each member, yes, but default constructing `unsigned char`s leaves them uninitialized. – John Kugelman Sep 08 '10 at 04:25
  • @John Kugelman: for idea #2, did you try a `const` reference? That may give the compiler the hint it needs to prevent unnecessary memory fetches. – Evan Teran Sep 08 '10 at 04:32
  • 9
    Just wanted to make a note that complexity of _parsing_ C++ (which is insanely complex, yes) has nothing to do with quality of optimization. The latter usually happens on stages where parse result is already transformed numerous times to a much more low-level representation. – Pavel Minaev Sep 08 '10 at 04:37
  • @Evan: A const ref won't work, since we need to assign to the object's members. – j_random_hacker Sep 08 '10 at 05:27
  • @j_random_hacker: ah, you are right, i didn't read the example code carefully enough. – Evan Teran Sep 08 '10 at 14:28
  • instead of `for (std::vector::iterator j = pixels.begin(); j != pixels.end(); ++j)`, use `for (std::vector::iterator j = pixels.begin(), k = pixels.end(); j < k; ++j)`. I observed a massive improvement in some architecture with the `<` instead of the `!=`. – Raffi Apr 13 '13 at 21:40
54

This is an old but popular question.

At this point, many programmers will be working in C++11. And in C++11 the OP's code as written runs equally fast for UseArray or UseVector.

UseVector completed in 3.74482 seconds
UseArray completed in 3.70414 seconds

The fundamental problem was that while your Pixel structure was uninitialized, std::vector<T>::resize( size_t, T const&=T() ) takes a default constructed Pixel and copies it. The compiler did not notice it was being asked to copy uninitialized data, so it actually performed the copy.

In C++11, std::vector<T>::resize has two overloads. The first is std::vector<T>::resize(size_t), the other is std::vector<T>::resize(size_t, T const&). This means when you invoke resize without a second argument, it simply default constructs, and the compiler is smart enough to realize that default construction does nothing, so it skips the pass over the buffer.

(The two overloads where added to handle movable, constructable and non-copyable types -- the performance improvement when working on uninitialized data is a bonus).

The push_back solution also does fencepost checking, which slows it down, so it remains slower than the malloc version.

live example (I also replaced the timer with chrono::high_resolution_clock).

Note that if you have a structure that usually requires initialization, but you want to handle it after growing your buffer, you can do this with a custom std::vector allocator. If you want to then move it into a more normal std::vector, I believe careful use of allocator_traits and overriding of == might pull that off, but am unsure.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Would also be interesting to see how `emplace_back` does vs `push_back` here. – Daniel Aug 30 '15 at 22:51
  • 1
    I cannot reproduce your results. Compiling your code `clang++ -std=c++11 -O3` has `UseArray completed in 2.02e-07 seconds` and `UseVector completed in 1.3026 seconds`. I also added an `UseVectorEmplaceBack` version which is approx. 2.5x as fast as `UseVectorPushBack`. – Daniel Aug 30 '15 at 23:12
  • 2
    @daniel odds are the optimizer removed everything from the array version. Always a risk with micro benchmarks. – Yakk - Adam Nevraumont Aug 30 '15 at 23:16
  • 4
    yes you're right, just looked at the assembly (or lack of it).. Should have probably thought of that given the ~6448514x difference! I wonder why the vector version can't make the same optimisation.. It does so if constructed with the dimensions rather than resized. – Daniel Aug 30 '15 at 23:20
  • On the site, you are correct, those values show up. However under clang-cl on windows maximum optimization with arch:AVX2 I get UseArray completed in 1e-07 seconds, UseVector completed in 1.00729 seconds. UseVectorPushBack completed in 1.4551 seconds. I suspect that the online compiler have to share the CPU with other process making any timing test null and void. I also suspect that memcpy is turned to a intrinsic when maximum optimizations are in place, while vector cannot do that. Need to look at the assembled code to make sure why. – rxantos Jun 29 '22 at 01:24
  • @rxantos 1e-7 just means the compiler worked out that UseArray wasn't doing anything and dead-code eliminated all of it. 1e-7 isn't "fast" it is "nothing was done, you just timed the action of taking a time stamp". – Yakk - Adam Nevraumont Jun 29 '22 at 02:47
34

To be fair, you cannot compare a C++ implementation to a C implementation, as I would call your malloc version. malloc does not create objects - it only allocates raw memory. That you then treat that memory as objects without calling the constructor is poor C++ (possibly invalid - I'll leave that to the language lawyers).

That said, simply changing the malloc to new Pixel[dimensions*dimensions] and free to delete [] pixels does not make much difference with the simple implementation of Pixel that you have. Here's the results on my box (E6600, 64-bit):

UseArray completed in 0.269 seconds
UseVector completed in 1.665 seconds
UseVectorPushBack completed in 7.309 seconds
The whole thing completed in 9.244 seconds

But with a slight change, the tables turn:

Pixel.h

struct Pixel
{
    Pixel();
    Pixel(unsigned char r, unsigned char g, unsigned char b);

    unsigned char r, g, b;
};

Pixel.cc

#include "Pixel.h"

Pixel::Pixel() {}
Pixel::Pixel(unsigned char r, unsigned char g, unsigned char b) 
  : r(r), g(g), b(b) {}

main.cc

#include "Pixel.h"
[rest of test harness without class Pixel]
[UseArray now uses new/delete not malloc/free]

Compiled this way:

$ g++ -O3 -c -o Pixel.o Pixel.cc
$ g++ -O3 -c -o main.o main.cc
$ g++ -o main main.o Pixel.o

we get very different results:

UseArray completed in 2.78 seconds
UseVector completed in 1.651 seconds
UseVectorPushBack completed in 7.826 seconds
The whole thing completed in 12.258 seconds

With a non-inlined constructor for Pixel, std::vector now beats a raw array.

It would appear that the complexity of allocation through std::vector and std:allocator is too much to be optimised as effectively as a simple new Pixel[n]. However, we can see that the problem is simply with the allocation not the vector access by tweaking a couple of the test functions to create the vector/array once by moving it outside the loop:

void UseVector()
{
    TestTimer t("UseVector");

    int dimension = 999;
    std::vector<Pixel> pixels;
    pixels.resize(dimension * dimension);

    for(int i = 0; i < 1000; ++i)
    {
        for(int i = 0; i < dimension * dimension; ++i)
        {
            pixels[i].r = 255;
            pixels[i].g = 0;
            pixels[i].b = 0;
        }
    }
}

and

void UseArray()
{
    TestTimer t("UseArray");

    int dimension = 999;
    Pixel * pixels = new Pixel[dimension * dimension];

    for(int i = 0; i < 1000; ++i)
    {
        for(int i = 0 ; i < dimension * dimension; ++i)
        {
            pixels[i].r = 255;
            pixels[i].g = 0;
            pixels[i].b = 0;
        }
    }
    delete [] pixels;
}

We get these results now:

UseArray completed in 0.254 seconds
UseVector completed in 0.249 seconds
UseVectorPushBack completed in 7.298 seconds
The whole thing completed in 7.802 seconds

What we can learn from this is that std::vector is comparable to a raw array for access, but if you need to create and delete the vector/array many times, creating a complex object will be more time consuming that creating a simple array when the element's constructor is not inlined. I don't think that this is very surprising.

camh
  • 40,988
  • 13
  • 62
  • 70
27

It was hardly a fair comparison when I first looked at your code; I definitely thought you weren't comparing apples with apples. So I thought, let's get constructors and destructors being called on all tests; and then compare.

const size_t dimension = 1000;

void UseArray() {
    TestTimer t("UseArray");
    for(size_t j = 0; j < dimension; ++j) {
        Pixel* pixels = new Pixel[dimension * dimension];
        for(size_t i = 0 ; i < dimension * dimension; ++i) {
            pixels[i].r = 255;
            pixels[i].g = 0;
            pixels[i].b = (unsigned char) (i % 255);
        }
        delete[] pixels;
    }
}

void UseVector() {
    TestTimer t("UseVector");
    for(size_t j = 0; j < dimension; ++j) {
        std::vector<Pixel> pixels(dimension * dimension);
        for(size_t i = 0; i < dimension * dimension; ++i) {
            pixels[i].r = 255;
            pixels[i].g = 0;
            pixels[i].b = (unsigned char) (i % 255);
        }
    }
}

int main() {
    TestTimer t1("The whole thing");

    UseArray();
    UseVector();

    return 0;
}

My thoughts were, that with this setup, they should be exactly the same. It turns out, I was wrong.

UseArray completed in 3.06 seconds
UseVector completed in 4.087 seconds
The whole thing completed in 10.14 seconds

So why did this 30% performance loss even occur? The STL has everything in headers, so it should have been possible for the compiler to understand everything that was required.

My thoughts were that it is in how the loop initialises all values to the default constructor. So I performed a test:

class Tester {
public:
    static int count;
    static int count2;
    Tester() { count++; }
    Tester(const Tester&) { count2++; }
};
int Tester::count = 0;
int Tester::count2 = 0;

int main() {
    std::vector<Tester> myvec(300);
    printf("Default Constructed: %i\nCopy Constructed: %i\n", Tester::count, Tester::count2);

    return 0;
}

The results were as I suspected:

Default Constructed: 1
Copy Constructed: 300

This is clearly the source of the slowdown, the fact that the vector uses the copy constructor to initialise the elements from a default constructed object.

This means, that the following pseudo-operation order is happening during construction of the vector:

Pixel pixel;
for (auto i = 0; i < N; ++i) vector[i] = pixel;

Which, due to the implicit copy constructor made by the compiler, is expanded to the following:

Pixel pixel;
for (auto i = 0; i < N; ++i) {
    vector[i].r = pixel.r;
    vector[i].g = pixel.g;
    vector[i].b = pixel.b;
}

So the default Pixel remains un-initialised, while the rest are initialised with the default Pixel's un-initialised values.

Compared to the alternative situation with New[]/Delete[]:

int main() {
    Tester* myvec = new Tester[300];

    printf("Default Constructed: %i\nCopy Constructed:%i\n", Tester::count, Tester::count2);

    delete[] myvec;

    return 0;
}

Default Constructed: 300
Copy Constructed: 0

They are all left to their un-initialised values, and without the double iteration over the sequence.

Armed with this information, how can we test it? Let's try over-writing the implicit copy constructor.

Pixel(const Pixel&) {}

And the results?

UseArray completed in 2.617 seconds
UseVector completed in 2.682 seconds
The whole thing completed in 5.301 seconds

So in summary, if you're making hundreds of vectors very often: re-think your algorithm.

In any case, the STL implementation isn't slower for some unknown reason, it just does exactly what you ask; hoping you know better.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
hiddensunset4
  • 5,825
  • 3
  • 39
  • 61
  • 3
    Judging from the fun we (you and I and other smart people here) have had, STL implemenation's "hope" is indeed a rather demanding one :P Basically, we can exaggerate and conclude that it hopes I've read and _analyzed_ all its source code. Anyway :P – kizzx2 Sep 01 '11 at 12:50
  • 1
    Awsome! In VS 2013 this made vector faster than arrays. Although it seems that for performance critical systems you need to test STL a lot to be able to use it effectively. – rozina Nov 17 '13 at 09:37
  • Beware that in some cases, compilers can see through the allocation and turn the loop into a nop: https://godbolt.org/z/sjeaWG5r8 I am curious why I don't see it doing the same with a `std::vector` but in production code, I shouldn't have hot loops that do work that get thrown away. :-D – Ben May 25 '22 at 13:36
26

Try with this:

void UseVectorCtor()
{
    TestTimer t("UseConstructor");

    for(int i = 0; i < 1000; ++i)
    {
        int dimension = 999;

        std::vector<Pixel> pixels(dimension * dimension, Pixel(255, 0, 0));
    }
}

I get almost exactly the same performance as with array.

The thing about vector is that it's a much more general tool than an array. And that means you have to consider how you use it. It can be used in a lot of different ways, providing functionality that an array doesn't even have. And if you use it "wrong" for your purpose, you incur a lot of overhead, but if you use it correctly, it is usually basically a zero-overhead data structure. In this case, the problem is that you separately initialized the vector (causing all elements to have their default ctor called), and then overwriting each element individually with the correct value. That is much harder for the compiler to optimize away than when you do the same thing with an array. Which is why the vector provides a constructor which lets you do exactly that: initialize N elements with value X.

And when you use that, the vector is just as fast as an array.

So no, you haven't busted the performance myth. But you have shown that it's only true if you use the vector optimally, which is a pretty good point too. :)

On the bright side, it's really the simplest usage that turns out to be fastest. If you contrast my code snippet (a single line) with John Kugelman's answer, containing heaps and heaps of tweaks and optimizations, which still don't quite eliminate the performance difference, it's pretty clear that vector is pretty cleverly designed after all. You don't have to jump through hoops to get speed equal to an array. On the contrary, you have to use the simplest possible solution.

jalf
  • 243,077
  • 51
  • 345
  • 550
  • 1
    I still question whether this is a fair comparison. If you're getting rid of the inner loop then the array equivalent would be to construct a single Pixel object and then blit that across the entire array. – John Kugelman Sep 08 '10 at 17:08
  • 1
    Using `new[]` performs the same default constructions that `vector.resize()` does, yet it is much faster. `new[]` + inner loop should be the same speed as `vector.resize()` + inner loop, but it's not, it's nearly twice as fast. – John Kugelman Sep 08 '10 at 17:10
  • @John: It is a fair comparison. In the original code, the array is allocated with `malloc` which doesn't initialize or construct anything, so it *is* effectively a single-pass algorithm just like my `vector` sample. And as for `new[]` the answer is obviously that both require two passes, but in the `new[]` case, the compiler is able to optimize that additional overhead away, which it doesn't do in the `vector` case. But I don't see why it is interesting what happens in suboptimal cases. If you care about performance, you don't write code like that. – jalf Sep 12 '10 at 01:43
  • @John: Interesting comment. If I wanted to blit across the entire array, I guess array is again the optimal solution -- since I can't tell `vector::resize()` to give me a contingous chunk of memory without it wasting time calling useless constructors. – kizzx2 Sep 12 '10 at 17:28
  • @kizzx2: yes and no. An array is normally initialized as well in C++. In C, you'd use `malloc` which doesn't perform initialization, but that won't work in C++ with non-POD types. So in the general case, a C++ array would be just as bad. Perhaps the question is, if you're going to perform this blitting often, wouldn't you reuse the same array/vector? And if you do that, then you only pay the cost of "useless constructors" once, at the very start. The actual blitting is just as fast after all. – jalf Sep 13 '10 at 12:23
  • But it is a good example. Like I said in my post, the standard library tries to be general. If you only need to store POD types **and** you need to perform something like blitting **and** for some reason you recreate the array each time **and** the size of the array is fixed, **then** a raw array allocated with `malloc` might be a better solution. (But avoiding the reallocation entirely would be better still, and would pretty much negate the performance penalty of the `vector`) – jalf Sep 13 '10 at 12:25
  • @jalf: What did you mean by "that won't work in C++ with non-POD types"? Do you mean struct with pointers in them? For me, though, I don't really see a reason why `vector` can't give me the option to just turn off the constructor if I don't want it. The decision was made so that it stores things contagious and `operator[]` works like array. Wonder why they left out this last bit :/ – kizzx2 Sep 13 '10 at 17:49
7

Try disabling checked iterators and building in release mode. You shouldn't see much of a performance difference.

kloffy
  • 2,928
  • 2
  • 25
  • 34
  • 1
    Tried `#define _SECURE_SCL 0`. That made `UseVector` somewhere around 4 seconds (similar to `gcc` below) but still it's twice as slow. – kizzx2 Sep 08 '10 at 03:03
  • This is almost certainly the cause. Microsoft graciously haves us iterator debugging on by default for both debug and release. We found this to be the root cause of a massive slowdown after upgrading from 2003 to 2008. Definitely one of the most pernicious gotchas of visual studio. – Doug T. Sep 08 '10 at 03:08
  • 2
    @kizzx2 there's another macro to disable: HAS_ITERATOR_DEBUGGING or some such. – Doug T. Sep 08 '10 at 03:14
  • As @Martin and my answers show, gcc shows the same pattern, even with optimization at `-O3`. – John Kugelman Sep 08 '10 at 03:17
  • 1
    @Doug: Looking at the doc, I think `_HAS_ITERATOR_DEBUGGING` is disabled in release build: http://msdn.microsoft.com/en-us/library/aa985939(VS.80).aspx – kizzx2 Sep 08 '10 at 03:19
4

GNU's STL (and others), given vector<T>(n), default constructs a prototypal object T() - the compiler will optimise away the empty constructor - but then a copy of whatever garbage happened to be in the memory addresses now reserved for the object is taken by the STL's __uninitialized_fill_n_aux, which loops populating copies of that object as the default values in the vector. So, "my" STL is not looping constructing, but constructing then loop/copying. It's counter intuitive, but I should have remembered as I commented on a recent stackoverflow question about this very point: the construct/copy can be more efficient for reference counted objects etc..

So:

vector<T> x(n);

or

vector<T> x;
x.resize(n);

is - on many STL implementations - something like:

T temp;
for (int i = 0; i < n; ++i)
    x[i] = temp;

The issue being that the current generation of compiler optimisers don't seem to work from the insight that temp is uninitialised garbage, and fail to optimise out the loop and default copy constructor invocations. You could credibly argue that compilers absolutely shouldn't optimise this away, as a programmer writing the above has a reasonable expectation that all the objects will be identical after the loop, even if garbage (usual caveats about 'identical'/operator== vs memcmp/operator= etc apply). The compiler can't be expected to have any extra insight into the larger context of std::vector<> or the later usage of the data that would suggest this optimisation safe.

This can be contrasted with the more obvious, direct implementation:

for (int i = 0; i < n; ++i)
    x[i] = T();

Which we can expect a compiler to optimise out.

To be a bit more explicit about the justification for this aspect of vector's behaviour, consider:

std::vector<big_reference_counted_object> x(10000);

Clearly it's a major difference if we make 10000 independent objects versus 10000 referencing the same data. There's a reasonable argument that the advantage of protecting casual C++ users from accidentally doing something so expensive outweights the very small real-world cost of hard-to-optimise copy construction.

ORIGINAL ANSWER (for reference / making sense of the comments): No chance. vector is as fast as an array, at least if you reserve space sensibly. ...

Tony Delroy
  • 102,968
  • 15
  • 177
  • 252
  • 6
    I really cannot justify this answer being anywhere slightly useful to anyone. I hope I could downvote twice. – kizzx2 Sep 08 '10 at 02:53
  • -1, there goes my support on kizzx2. vector never going to be as fast as array due to the additional feature it provides, rule of universe, everything has a price ! – YeenFei Sep 08 '10 at 03:01
  • You're missing out, Tony… it is an example of an artificial benchmark, but it does prove what it purports to. – Potatoswatter Sep 08 '10 at 05:13
  • Roses are green, violets are orange, the downvotes are bitter, but the answer begs them. – Pavel Minaev Sep 08 '10 at 05:32
3

Martin York's answer bothers me because it seems like an attempt to brush the initialisation problem under the carpet. But he is right to identify redundant default construction as the source of performance problems.

[EDIT: Martin's answer no longer suggests changing the default constructor.]

For the immediate problem at hand, you could certainly call the 2-parameter version of the vector<Pixel> ctor instead:

std::vector<Pixel> pixels(dimension * dimension, Pixel(255, 0, 0));

That works if you want to initialise with a constant value, which is a common case. But the more general problem is: How can you efficiently initialise with something more complicated than a constant value?

For this you can use a back_insert_iterator, which is an iterator adaptor. Here's an example with a vector of ints, although the general idea works just as well for Pixels:

#include <iterator>
// Simple functor return a list of squares: 1, 4, 9, 16...
struct squares {
    squares() { i = 0; }
    int operator()() const { ++i; return i * i; }

private:
    int i;
};

...

std::vector<int> v;
v.reserve(someSize);     // To make insertions efficient
std::generate_n(std::back_inserter(v), someSize, squares());

Alternatively you could use copy() or transform() instead of generate_n().

The downside is that the logic to construct the initial values needs to be moved into a separate class, which is less convenient than having it in-place (although lambdas in C++1x make this much nicer). Also I expect this will still not be as fast as a malloc()-based non-STL version, but I expect it will be close, since it only does one construction for each element.

Community
  • 1
  • 1
j_random_hacker
  • 50,331
  • 10
  • 105
  • 169
2

The vector ones are additionally calling Pixel constructors.

Each is causing almost a million ctor runs that you're timing.

edit: then there's the outer 1...1000 loop, so make that a billion ctor calls!

edit 2: it'd be interesting to see the disassembly for the UseArray case. An optimizer could optimize the whole thing away, since it has no effect other than burning CPU.

Graham Perks
  • 23,007
  • 8
  • 61
  • 83
  • You're right, but the question is: how can these pointless ctor calls be turned off? It's easy for the non-STL approach, but difficult/ugly for the STL way. – j_random_hacker Sep 08 '10 at 04:16
1

Here's how the push_back method in vector works:

  1. The vector allocates X amount of space when it is initialized.
  2. As stated below it checks if there is room in the current underlying array for the item.
  3. It makes a copy of the item in the push_back call.

After calling push_back X items:

  1. The vector reallocates kX amount of space into a 2nd array.
  2. It Copies the entries of the first array onto the second.
  3. Discards the first array.
  4. Now uses the second array as storage until it reaches kX entries.

Repeat. If you're not reserving space its definitely going to be slower. More than that, if it's expensive to copy the item then 'push_back' like that is going to eat you alive.

As to the vector versus array thing, I'm going to have to agree with the other people. Run in release, turn optimizations on, and put in a few more flags so that the friendly people at Microsoft don't #@%$^ it up for ya.

One more thing, if you don't need to resize, use Boost.Array.

wheaties
  • 35,646
  • 15
  • 94
  • 131
  • I understand people don't like to read a bunch of code when it's posted verbatim. But I did use `reserve` like I should. – kizzx2 Sep 08 '10 at 02:57
  • Sorry I missed it. Was nothing else I put up there helpful at all? – wheaties Sep 08 '10 at 03:54
  • `push_back` has amortized constant time. It sounds like you're describing an O(N) process. (Steps 1 and 3 seem completely out of place.) What makes `push_back` slow for OP is the range check to see whether reallocation needs to happen, updating the pointers, the check against NULL inside placement `new`, and other little things that normally get drowned out by the program's actual work. – Potatoswatter Sep 08 '10 at 05:11
  • It's going to be slower even with `reserve` as it still has to make that check (whether it needs to realloc) on every `push_back`. – Pavel Minaev Sep 08 '10 at 05:12
  • All good points. What I'm describing sounds like an O(N) process but it's not, well not quite. Most people I know do not understand how a `vector` performs it's resize functionality, it's just "magic." Here, let me clarify it a bit more. – wheaties Sep 08 '10 at 11:56
1

Some profiler data (pixel is aligned to 32 bits):

g++ -msse3 -O3 -ftree-vectorize -g test.cpp -DNDEBUG && ./a.out
UseVector completed in 3.123 seconds
UseArray completed in 1.847 seconds
UseVectorPushBack completed in 9.186 seconds
The whole thing completed in 14.159 seconds

Blah

andrey@nv:~$ opannotate --source libcchem/src/a.out  | grep "Total samples for file" -A3
Overflow stats not available
 * Total samples for file : "/usr/include/c++/4.4/ext/new_allocator.h"
 *
 * 141008 52.5367
 */
--
 * Total samples for file : "/home/andrey/libcchem/src/test.cpp"
 *
 *  61556 22.9345
 */
--
 * Total samples for file : "/usr/include/c++/4.4/bits/stl_vector.h"
 *
 *  41956 15.6320
 */
--
 * Total samples for file : "/usr/include/c++/4.4/bits/stl_uninitialized.h"
 *
 *  20956  7.8078
 */
--
 * Total samples for file : "/usr/include/c++/4.4/bits/stl_construct.h"
 *
 *   2923  1.0891
 */

In allocator:

               :      // _GLIBCXX_RESOLVE_LIB_DEFECTS
               :      // 402. wrong new expression in [some_] allocator::construct
               :      void
               :      construct(pointer __p, const _Tp& __val)
141008 52.5367 :      { ::new((void *)__p) _Tp(__val); }

vector:

               :void UseVector()
               :{ /* UseVector() total:  60121 22.3999 */
...
               :
               :
 10790  4.0201 :        for (int i = 0; i < dimension * dimension; ++i) {
               :
   495  0.1844 :            pixels[i].r = 255;
               :
 12618  4.7012 :            pixels[i].g = 0;
               :
  2253  0.8394 :            pixels[i].b = 0;
               :
               :        }

array

               :void UseArray()
               :{ /* UseArray() total:  35191 13.1114 */
               :
...
               :
   136  0.0507 :        for (int i = 0; i < dimension * dimension; ++i) {
               :
  9897  3.6874 :            pixels[i].r = 255;
               :
  3511  1.3081 :            pixels[i].g = 0;
               :
 21647  8.0652 :            pixels[i].b = 0;

Most of the overhead is in the copy constructor. For example,

    std::vector < Pixel > pixels;//(dimension * dimension, Pixel());

    pixels.reserve(dimension * dimension);

    for (int i = 0; i < dimension * dimension; ++i) {

        pixels[i].r = 255;

        pixels[i].g = 0;

        pixels[i].b = 0;
    }

It has the same performance as an array.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Anycorn
  • 50,217
  • 42
  • 167
  • 261
  • 2
    Unfortunately, after the "solution" you gave, `pixels.size()` will be broken. – kizzx2 Aug 07 '11 at 23:44
  • 1
    this is wrong, you can't call reserve and then use the elements, you must still use push_back to add items – paulm Jan 14 '14 at 22:41
1

A better benchmark (I think...), compiler due to optimizations can change code, becouse results of allocated vectors/arrays are not used anywhere. Results:

$ g++ test.cpp -o test -O3 -march=native
$ ./test 
UseArray inner completed in 0.652 seconds
UseArray completed in 0.773 seconds
UseVector inner completed in 0.638 seconds
UseVector completed in 0.757 seconds
UseVectorPushBack inner completed in 6.732 seconds
UseVectorPush completed in 6.856 seconds
The whole thing completed in 8.387 seconds

Compiler:

gcc version 6.2.0 20161019 (Debian 6.2.0-9)

CPU:

model name  : Intel(R) Core(TM) i7-3630QM CPU @ 2.40GHz

And the code:

#include <cstdlib>
#include <vector>

#include <iostream>
#include <string>

#include <boost/date_time/posix_time/ptime.hpp>
#include <boost/date_time/microsec_time_clock.hpp>

class TestTimer
{
    public:
        TestTimer(const std::string & name) : name(name),
            start(boost::date_time::microsec_clock<boost::posix_time::ptime>::local_time())
        {
        }

        ~TestTimer()
        {
            using namespace std;
            using namespace boost;

            posix_time::ptime now(date_time::microsec_clock<posix_time::ptime>::local_time());
            posix_time::time_duration d = now - start;

            cout << name << " completed in " << d.total_milliseconds() / 1000.0 <<
                " seconds" << endl;
        }

    private:
        std::string name;
        boost::posix_time::ptime start;
};

struct Pixel
{
    Pixel()
    {
    }

    Pixel(unsigned char r, unsigned char g, unsigned char b) : r(r), g(g), b(b)
    {
    }

    unsigned char r, g, b;
};

void UseVector(std::vector<std::vector<Pixel> >& results)
{
    TestTimer t("UseVector inner");

    for(int i = 0; i < 1000; ++i)
    {
        int dimension = 999;

        std::vector<Pixel>& pixels = results.at(i);
        pixels.resize(dimension * dimension);

        for(int i = 0; i < dimension * dimension; ++i)
        {
            pixels[i].r = 255;
            pixels[i].g = 0;
            pixels[i].b = 0;
        }
    }
}

void UseVectorPushBack(std::vector<std::vector<Pixel> >& results)
{
    TestTimer t("UseVectorPushBack inner");

    for(int i = 0; i < 1000; ++i)
    {
        int dimension = 999;

        std::vector<Pixel>& pixels = results.at(i);
            pixels.reserve(dimension * dimension);

        for(int i = 0; i < dimension * dimension; ++i)
            pixels.push_back(Pixel(255, 0, 0));
    }
}

void UseArray(Pixel** results)
{
    TestTimer t("UseArray inner");

    for(int i = 0; i < 1000; ++i)
    {
        int dimension = 999;

        Pixel * pixels = (Pixel *)malloc(sizeof(Pixel) * dimension * dimension);

        results[i] = pixels;

        for(int i = 0 ; i < dimension * dimension; ++i)
        {
            pixels[i].r = 255;
            pixels[i].g = 0;
            pixels[i].b = 0;
        }

        // free(pixels);
    }
}

void UseArray()
{
    TestTimer t("UseArray");
    Pixel** array = (Pixel**)malloc(sizeof(Pixel*)* 1000);
    UseArray(array);
    for(int i=0;i<1000;++i)
        free(array[i]);
    free(array);
}

void UseVector()
{
    TestTimer t("UseVector");
    {
        std::vector<std::vector<Pixel> > vector(1000, std::vector<Pixel>());
        UseVector(vector);
    }
}

void UseVectorPushBack()
{
    TestTimer t("UseVectorPush");
    {
        std::vector<std::vector<Pixel> > vector(1000, std::vector<Pixel>());
        UseVectorPushBack(vector);
    }
}


int main()
{
    TestTimer t1("The whole thing");

    UseArray();
    UseVector();
    UseVectorPushBack();

    return 0;
}
1

I did some extensive tests that I wanted to for a while now. Might as well share this.

This is my dual boot machine i7-3770, 16GB Ram, x86_64, on Windows 8.1 and on Ubuntu 16.04. More information and conclusions, remarks below. Tested both MSVS 2017 and g++ (both on Windows and on Linux).

Test Program

#include <iostream>
#include <chrono>
//#include <algorithm>
#include <array>
#include <locale>
#include <vector>
#include <queue>
#include <deque>

// Note: total size of array must not exceed 0x7fffffff B = 2,147,483,647B
//  which means that largest int array size is 536,870,911
// Also image size cannot be larger than 80,000,000B
constexpr int long g_size = 100000;
int g_A[g_size];


int main()
{
    std::locale loc("");
    std::cout.imbue(loc);
    constexpr int long size = 100000;  // largest array stack size

    // stack allocated c array
    std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
    int A[size];
    for (int i = 0; i < size; i++)
        A[i] = i;

    auto duration = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - start).count();
    std::cout << "c-style stack array duration=" << duration / 1000.0 << "ms\n";
    std::cout << "c-style stack array size=" << sizeof(A) << "B\n\n";

    // global stack c array
    start = std::chrono::steady_clock::now();
    for (int i = 0; i < g_size; i++)
        g_A[i] = i;

    duration = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - start).count();
    std::cout << "global c-style stack array duration=" << duration / 1000.0 << "ms\n";
    std::cout << "global c-style stack array size=" << sizeof(g_A) << "B\n\n";

    // raw c array heap array
    start = std::chrono::steady_clock::now();
    int* AA = new int[size];    // bad_alloc() if it goes higher than 1,000,000,000
    for (int i = 0; i < size; i++)
        AA[i] = i;

    duration = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - start).count();
    std::cout << "c-style heap array duration=" << duration / 1000.0 << "ms\n";
    std::cout << "c-style heap array size=" << sizeof(AA) << "B\n\n";
    delete[] AA;

    // std::array<>
    start = std::chrono::steady_clock::now();
    std::array<int, size> AAA;
    for (int i = 0; i < size; i++)
        AAA[i] = i;
    //std::sort(AAA.begin(), AAA.end());

    duration = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - start).count();
    std::cout << "std::array duration=" << duration / 1000.0 << "ms\n";
    std::cout << "std::array size=" << sizeof(AAA) << "B\n\n";

    // std::vector<>
    start = std::chrono::steady_clock::now();
    std::vector<int> v;
    for (int i = 0; i < size; i++)
        v.push_back(i);
    //std::sort(v.begin(), v.end());

    duration = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - start).count();
    std::cout << "std::vector duration=" << duration / 1000.0 << "ms\n";
    std::cout << "std::vector size=" << v.size() * sizeof(v.back()) << "B\n\n";

    // std::deque<>
    start = std::chrono::steady_clock::now();
    std::deque<int> dq;
    for (int i = 0; i < size; i++)
        dq.push_back(i);
    //std::sort(dq.begin(), dq.end());

    duration = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - start).count();
    std::cout << "std::deque duration=" << duration / 1000.0 << "ms\n";
    std::cout << "std::deque size=" << dq.size() * sizeof(dq.back()) << "B\n\n";

    // std::queue<>
    start = std::chrono::steady_clock::now();
    std::queue<int> q;
    for (int i = 0; i < size; i++)
        q.push(i);

    duration = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - start).count();
    std::cout << "std::queue duration=" << duration / 1000.0 << "ms\n";
    std::cout << "std::queue size=" << q.size() * sizeof(q.front()) << "B\n\n";
}

Results

//////////////////////////////////////////////////////////////////////////////////////////
// with MSVS 2017:
// >> cl /std:c++14 /Wall -O2 array_bench.cpp
//
// c-style stack array duration=0.15ms
// c-style stack array size=400,000B
//
// global c-style stack array duration=0.130ms
// global c-style stack array size=400,000B
//
// c-style heap array duration=0.90ms
// c-style heap array size=4B
//
// std::array duration=0.20ms
// std::array size=400,000B
//
// std::vector duration=0.544ms
// std::vector size=400,000B
//
// std::deque duration=1.375ms
// std::deque size=400,000B
//
// std::queue duration=1.491ms
// std::queue size=400,000B
//
//////////////////////////////////////////////////////////////////////////////////////////
//
// with g++ version:
//      - (tdm64-1) 5.1.0 on Windows
//      - (Ubuntu 5.4.0-6ubuntu1~16.04.10) 5.4.0 20160609 on Ubuntu 16.04
// >> g++ -std=c++14 -Wall -march=native -O2 array_bench.cpp -o array_bench
//
// c-style stack array duration=0ms
// c-style stack array size=400,000B
//
// global c-style stack array duration=0.124ms
// global c-style stack array size=400,000B
//
// c-style heap array duration=0.648ms
// c-style heap array size=8B
//
// std::array duration=1ms
// std::array size=400,000B
//
// std::vector duration=0.402ms
// std::vector size=400,000B
//
// std::deque duration=0.234ms
// std::deque size=400,000B
//
// std::queue duration=0.304ms
// std::queue size=400,000
//
//////////////////////////////////////////////////////////////////////////////////////////

Notes

  • Assembled by an average of 10 runs.
  • I initially performed tests with std::sort() too (you can see it commented out) but removed them later because there were no significant relative differences.

My Conclusions and Remarks

  • notice how global c-style array takes almost as much time as the heap c-style array
  • Out of all tests I noticed a remarkable stability in std::array's time variations between consecutive runs, while others especially std:: data structs varied wildly in comparison
  • O3 optimization didn't show any noteworthy time differences
  • Removing optimization on Windows cl (no -O2) and on g++ (Win/Linux no -O2, no -march=native) increases times SIGNIFICANTLY. Particularly for std::data structs. Overall higher times on MSVS than g++, but std::array and c-style arrays faster on Windows without optimization
  • g++ produces faster code than microsoft's compiler (apparently it runs faster even on Windows).

Verdict

Of course this is code for an optimized build. And since the question was about std::vector then yes it is !much! slower than plain arrays (optimized/unoptimized). But when you're doing a benchmark, you naturally want to produce optimized code.

The star of the show for me though has been std::array.

KeyC0de
  • 4,728
  • 8
  • 44
  • 68
1

My laptop is Lenova G770 (4 GB RAM).

The OS is Windows 7 64-bit (the one with laptop)

Compiler is MinGW 4.6.1.

The IDE is Code::Blocks.

I test the source codes of the first post.

The results

O2 optimization

UseArray completed in 2.841 seconds

UseVector completed in 2.548 seconds

UseVectorPushBack completed in 11.95 seconds

The whole thing completed in 17.342 seconds

system pause

O3 optimization

UseArray completed in 1.452 seconds

UseVector completed in 2.514 seconds

UseVectorPushBack completed in 12.967 seconds

The whole thing completed in 16.937 seconds

It looks like the performance of vector is worse under O3 optimization.

If you change the loop to

    pixels[i].r = i;
    pixels[i].g = i;
    pixels[i].b = i;

The speed of array and vector under O2 and O3 are almost the same.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
StereoMatching
  • 4,971
  • 6
  • 38
  • 70
  • Even I change malloc to new, in the first test case under O3, the performance of vector still slower than array.But when you change the assign value from (255, 0, 0) to (i, i, i), performance of vector and array are almost the same under O2 and O3, it is pretty weird – StereoMatching Mar 20 '12 at 15:45
  • Sorry, I forget to change free to delete.After changing free to delete, the performance under O3 of vector and array are the same now, looks like allocator is the main reason? – StereoMatching Mar 20 '12 at 15:55
0

By the way the slow down your seeing in classes using vector also occurs with standard types like int. Heres a multithreaded code:

#include <iostream>
#include <cstdio>
#include <map>
#include <string>
#include <typeinfo>
#include <vector>
#include <pthread.h>
#include <sstream>
#include <fstream>
using namespace std;

//pthread_mutex_t map_mutex=PTHREAD_MUTEX_INITIALIZER;

long long num=500000000;
int procs=1;

struct iterate
{
    int id;
    int num;
    void * member;
    iterate(int a, int b, void *c) : id(a), num(b), member(c) {}
};

//fill out viterate and piterate
void * viterate(void * input)
{
    printf("am in viterate\n");
    iterate * info=static_cast<iterate *> (input);
    // reproduce member type
    vector<int> test= *static_cast<vector<int>*> (info->member);
    for (int i=info->id; i<test.size(); i+=info->num)
    {
        //printf("am in viterate loop\n");
        test[i];
    }
    pthread_exit(NULL);
}

void * piterate(void * input)
{
    printf("am in piterate\n");
    iterate * info=static_cast<iterate *> (input);;
    int * test=static_cast<int *> (info->member);
    for (int i=info->id; i<num; i+=info->num) {
        //printf("am in piterate loop\n");
        test[i];
    }
    pthread_exit(NULL);
}

int main()
{
    cout<<"producing vector of size "<<num<<endl;
    vector<int> vtest(num);
    cout<<"produced  a vector of size "<<vtest.size()<<endl;
    pthread_t thread[procs];

    iterate** it=new iterate*[procs];
    int ans;
    void *status;

    cout<<"begining to thread through the vector\n";
    for (int i=0; i<procs; i++) {
        it[i]=new iterate(i, procs, (void *) &vtest);
    //  ans=pthread_create(&thread[i],NULL,viterate, (void *) it[i]);
    }
    for (int i=0; i<procs; i++) {
        pthread_join(thread[i], &status);
    }
    cout<<"end of threading through the vector";
    //reuse the iterate structures

    cout<<"producing a pointer with size "<<num<<endl;
    int * pint=new int[num];
    cout<<"produced a pointer with size "<<num<<endl;

    cout<<"begining to thread through the pointer\n";
    for (int i=0; i<procs; i++) {
        it[i]->member=&pint;
        ans=pthread_create(&thread[i], NULL, piterate, (void*) it[i]);
    }
    for (int i=0; i<procs; i++) {
        pthread_join(thread[i], &status);
    }
    cout<<"end of threading through the pointer\n";

    //delete structure array for iterate
    for (int i=0; i<procs; i++) {
        delete it[i];
    }
    delete [] it;

    //delete pointer
    delete [] pint;

    cout<<"end of the program"<<endl;
    return 0;
}

The behavior from the code shows the instantiation of vector is the longest part of the code. Once you get through that bottle neck. The rest of the code runs extremely fast. This is true no matter how many threads you are running on.

By the way ignore the absolutely insane number of includes. I have been using this code to test things for a project so the number of includes keep growing.

Zachary Kraus
  • 1,051
  • 10
  • 21
0

I just want to mention that vector (and smart_ptr) is just a thin layer add on top of raw arrays (and raw pointers). And actually the access time of an vector in continuous memory is faster than array. The following code shows the result of initialize and access vector and array.

#include <boost/date_time/posix_time/posix_time.hpp>
#include <iostream>
#include <vector>
#define SIZE 20000
int main() {
    srand (time(NULL));
    vector<vector<int>> vector2d;
    vector2d.reserve(SIZE);
    int index(0);
    boost::posix_time::ptime start_total = boost::posix_time::microsec_clock::local_time();
    //  timer start - build + access
    for (int i = 0; i < SIZE; i++) {
        vector2d.push_back(vector<int>(SIZE));
    }
    boost::posix_time::ptime start_access = boost::posix_time::microsec_clock::local_time();
    //  timer start - access
    for (int i = 0; i < SIZE; i++) {
        index = rand()%SIZE;
        for (int j = 0; j < SIZE; j++) {

            vector2d[index][index]++;
        }
    }
    boost::posix_time::ptime end = boost::posix_time::microsec_clock::local_time();
    boost::posix_time::time_duration msdiff = end - start_total;
    cout << "Vector total time: " << msdiff.total_milliseconds() << "milliseconds.\n";
    msdiff = end - start_acess;
    cout << "Vector access time: " << msdiff.total_milliseconds() << "milliseconds.\n"; 


    int index(0);
    int** raw2d = nullptr;
    raw2d = new int*[SIZE];
    start_total = boost::posix_time::microsec_clock::local_time();
    //  timer start - build + access
    for (int i = 0; i < SIZE; i++) {
        raw2d[i] = new int[SIZE];
    }
    start_access = boost::posix_time::microsec_clock::local_time();
    //  timer start - access
    for (int i = 0; i < SIZE; i++) {
        index = rand()%SIZE;
        for (int j = 0; j < SIZE; j++) {

            raw2d[index][index]++;
        }
    }
    end = boost::posix_time::microsec_clock::local_time();
    msdiff = end - start_total;
    cout << "Array total time: " << msdiff.total_milliseconds() << "milliseconds.\n";
    msdiff = end - start_acess;
    cout << "Array access time: " << msdiff.total_milliseconds() << "milliseconds.\n"; 
    for (int i = 0; i < SIZE; i++) {
        delete [] raw2d[i];
    }
    return 0;
}

The output is:

    Vector total time: 925milliseconds.
    Vector access time: 4milliseconds.
    Array total time: 30milliseconds.
    Array access time: 21milliseconds.

So the speed will be almost the same if you use it properly. (as others mentioned using reserve() or resize()).

Charles Chow
  • 575
  • 1
  • 8
  • 12
0

Well, because vector::resize() does much more processing than plain memory allocation (by malloc).

Try to put a breakpoint in your copy constructor (define it so that you can breakpoint!) and there goes the additional processing time.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
YeenFei
  • 3,180
  • 18
  • 26
0

With the right options, vectors and arrays can generate identical asm. In these cases, they are of course the same speed, because you get the same executable file either way.

  • 1
    In this case, they do not seem to generate the same assembly. In particular, there seems to be no way to suppress the call to constructors using vectors. You can refer to the answers here for that problem (it's a long read but it does explain why there is a performance difference in cases other than the simples test case in the link you provdeded.) (actually, there seems to be a way -- writing a custom STL allocator, as suggested. Personally, I find it unnecessarily more work than using malloc) – kizzx2 Sep 25 '10 at 16:26
  • 1
    @kizzx2: That you have to go to such lengths to use *unconstructed* objects is a good thing, because that's an error 99% (I may be grossly underestimating) of the time. I did read the other answers, and I realize I don't address your specific situation (no need, the other answers are correct), but I still wanted to provide you with this example of how vectors and arrays can behave exactly the same. –  Sep 25 '10 at 19:54
0

I Have to say I'm not an expert in C++. But to add some experiments results:

compile: gcc-6.2.0/bin/g++ -O3 -std=c++14 vector.cpp

machine:

Intel(R) Xeon(R) CPU E5-2690 v2 @ 3.00GHz 

OS:

2.6.32-642.13.1.el6.x86_64

Output:

UseArray completed in 0.167821 seconds
UseVector completed in 0.134402 seconds
UseConstructor completed in 0.134806 seconds
UseFillConstructor completed in 1.00279 seconds
UseVectorPushBack completed in 6.6887 seconds
The whole thing completed in 8.12888 seconds

Here the only thing I feel strange is that "UseFillConstructor" performance compared with "UseConstructor".

The code:

void UseConstructor()
{
    TestTimer t("UseConstructor");

    for(int i = 0; i < 1000; ++i)
    {
        int dimension = 999;

        std::vector<Pixel> pixels(dimension*dimension);
        for(int i = 0; i < dimension * dimension; ++i)
        {
            pixels[i].r = 255;
            pixels[i].g = 0;
            pixels[i].b = 0;
        }
    }
}


void UseFillConstructor()
{
    TestTimer t("UseFillConstructor");

    for(int i = 0; i < 1000; ++i)
    {
        int dimension = 999;

        std::vector<Pixel> pixels(dimension*dimension, Pixel(255,0,0));
    }
}

So the additional "value" provided slows down performance quite a lot, which I think is due to multiple call to copy constructor. But...

Compile:

gcc-6.2.0/bin/g++ -std=c++14 -O vector.cpp

Output:

UseArray completed in 1.02464 seconds
UseVector completed in 1.31056 seconds
UseConstructor completed in 1.47413 seconds
UseFillConstructor completed in 1.01555 seconds
UseVectorPushBack completed in 6.9597 seconds
The whole thing completed in 11.7851 seconds

So in this case, gcc optimization is very important but it can't help you much when a value is provided as default. This, is against my tuition actually. Hopefully it helps new programmer when choose which vector initialization format.

user2189731
  • 558
  • 8
  • 15
0

It seems to depend on the compiler flags. Here is a benchmark code:

#include <chrono>
#include <cmath>
#include <ctime>
#include <iostream>
#include <vector>


int main(){

    int size = 1000000; // reduce this number in case your program crashes
    int L = 10;

    std::cout << "size=" << size << " L=" << L << std::endl;
    {
        srand( time(0) );
        double * data = new double[size];
        double result = 0.;
        std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
        for( int l = 0; l < L; l++ ) {
            for( int i = 0; i < size; i++ ) data[i] = rand() % 100;
            for( int i = 0; i < size; i++ ) result += data[i] * data[i];
        }
        std::chrono::steady_clock::time_point end   = std::chrono::steady_clock::now();
        auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();
        std::cout << "Calculation result is " << sqrt(result) << "\n";
        std::cout << "Duration of C style heap array:    " << duration << "ms\n";
        delete data;
    }

    {
        srand( 1 + time(0) );
        double data[size]; // technically, non-compliant with C++ standard.
        double result = 0.;
        std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
        for( int l = 0; l < L; l++ ) {
            for( int i = 0; i < size; i++ ) data[i] = rand() % 100;
            for( int i = 0; i < size; i++ ) result += data[i] * data[i];
        }
        std::chrono::steady_clock::time_point end   = std::chrono::steady_clock::now();
        auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();
        std::cout << "Calculation result is " << sqrt(result) << "\n";
        std::cout << "Duration of C99 style stack array: " << duration << "ms\n";
    }

    {
        srand( 2 + time(0) );
        std::vector<double> data( size );
        double result = 0.;
        std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
        for( int l = 0; l < L; l++ ) {
            for( int i = 0; i < size; i++ ) data[i] = rand() % 100;
            for( int i = 0; i < size; i++ ) result += data[i] * data[i];
        }
        std::chrono::steady_clock::time_point end   = std::chrono::steady_clock::now();
        auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();
        std::cout << "Calculation result is " << sqrt(result) << "\n";
        std::cout << "Duration of std::vector array:     " << duration << "ms\n";
    }

    return 0;
}

Different optimization flags give different answers:

$ g++ -O0 benchmark.cpp 
$ ./a.out 
size=1000000 L=10
Calculation result is 181182
Duration of C style heap array:    118441ms
Calculation result is 181240
Duration of C99 style stack array: 104920ms
Calculation result is 181210
Duration of std::vector array:     124477ms
$g++ -O3 benchmark.cpp
$ ./a.out 
size=1000000 L=10
Calculation result is 181213
Duration of C style heap array:    107803ms
Calculation result is 181198
Duration of C99 style stack array: 87247ms
Calculation result is 181204
Duration of std::vector array:     89083ms
$ g++ -Ofast benchmark.cpp 
$ ./a.out 
size=1000000 L=10
Calculation result is 181164
Duration of C style heap array:    93530ms
Calculation result is 181179
Duration of C99 style stack array: 80620ms
Calculation result is 181191
Duration of std::vector array:     78830ms

Your exact results will vary but this is quite typical on my machine.

shuhalo
  • 5,732
  • 12
  • 43
  • 60
0

In my experience, sometimes, just sometimes, vector<int> can be many times slower than int[]. One thing to keep mind is that vectors of vectors are very unlike int[][]. As the elements are probably not contiguous in memory. This means you can resize different vectors inside of the main one, but CPU might not be able to cache elements as well as in the case of int[][].

Íhor Mé
  • 896
  • 9
  • 13