0

I have implemented a rudimentary vector using the code from the Weiss C++ textbook on data structures (see below). when i time it with 100,000 push_backs it takes 0.001 seconds.

when i do the exact same experiment using the stl::vector, it takes 0.008 seconds (roughly 8x slower). does anyone know why this is? thanks

#include <iostream>
#include <algorithm>
#include <ctime>
#include <vector>

template<typename Object>
class Vector {

public:

    // normal constructor
    explicit Vector(int initialSize = 0) :
        theSize{ initialSize }, theCapacity{ initialSize + SPARE_CAPACITY },
        objects{ new Object[theCapacity] }
    {}

    // copy constructor
    Vector(const Vector& rhs) :
        theSize{ rhs.theSize }, theCapacity{ rhs.theCapacity }, objects{ nullptr }
    {
        objects = new Object[theCapacity];
        for (int k = 0; k < theSize; ++k)
            objects[k] = rhs.objects[k];
    }

    // copy assignment operator
    Vector& operator=(const Vector& rhs)
    {
        Vector copy = rhs;
        std::swap(*this, copy);
        return *this;
    }

    // destructor
    ~Vector()
    {
        delete[] objects;
    }

    // move constructor
    Vector(Vector&& rhs) :
        theSize{ rhs.theSize }, theCapacity{ rhs.theCapacity }, objects{ rhs.objects }
    {
        rhs.objects = nullptr;
        rhs.theSize = 0;
        rhs.theCapacity = 0;
    }

    // move assignment operator
    Vector& operator=(Vector&& rhs)
    {
        std::swap(theSize, rhs.theSize);
        std::swap(theCapacity, rhs.theCapacity);
        std::swap(objects, rhs.objects);

        return *this;
    }

    void resize(int newSize)
    {
        if (newSize > theCapacity)
            reserve(newSize * 2); // talk about amortized time (python book)
        theSize = newSize;
    }

    void reserve(int newCapacity)
    {
        if (newCapacity < theSize)
            return;

        Object* newArray = new Object[newCapacity];
        for (int k = 0; k < theSize; ++k)
            newArray[k] = std::move(objects[k]);

        theCapacity = newCapacity;
        std::swap(objects, newArray);
        delete[] newArray;
    }

    Object& operator[](int index)
    {
        return objects[index];
    }

    const Object& operator[](int index)const
    {
        return objects[index];
    }

    bool empty() const
    {
        return size() == 0;
    }

    int size() const
    {
        return theSize;
    }

    int capacity() const
    {
        return theCapacity; 
    }

    void push_back(const Object& x)
    {
        if (theSize == theCapacity)
            reserve(2 * theCapacity + 1);

        objects[theSize++] = x; 
    }

    void push_back(Object&& x)
    {
        if (theSize == theCapacity)
            reserve(2 * theCapacity + 1);

        objects[theSize++] = std::move(x); 
    }

    void pop_back()
    {
        --theSize;
    }

    const Object& back() const
    {
        return objects[theSize - 1];
    }

    // iterator
    typedef Object* iterator;
    typedef const Object* const_iterator;

    iterator begin()
    {
        return &objects[0];
    }
    const_iterator begin() const
    {
        return &objects[0];
    }
    iterator end()
    {
        return &objects[size()];
    }
    const_iterator end() const
    {
        return &objects[size()];
    }

    static const int SPARE_CAPACITY = 16; 

private:
    int theSize;
    int theCapacity;
    Object* objects; 
};


int main()
{
    std::clock_t start;
    start = std::clock(); 
    std::vector<int> vec2{ 0 };
    for (int i = 0; i < 100000; i++)
        vec2.push_back(i);
    double duration = (std::clock() - start) / (double)CLOCKS_PER_SEC;

    std::cout << "printf: " << duration << '\n';


    start = std::clock();       
    Vector<int> vec{ 0 };
    for (int i = 0; i < 100000; i++)
        vec.push_back(i);

    duration = (std::clock() - start) / (double)CLOCKS_PER_SEC;

    std::cout << "printf: " << duration << '\n';


    
    
}
Russell Butler
  • 326
  • 4
  • 15
  • 1
    `std::vector` uses a somewhat arbitrary constant to control the rate that it grows when it doesn't have enough storage. A large growth rate wastes space. A small growth rate wastes time. The growth rate of your vector is larger than most implementations. – Drew Dormann Jan 25 '23 at 01:08
  • thanks. do you know the growth rate? where can i find it? – Russell Butler Jan 25 '23 at 01:10
  • 3
    What did you use for your optimization level? `-O2`? `-O3`? – Eljay Jan 25 '23 at 01:11
  • just compiled it using debug mode in visual studio, so i don't know. – Russell Butler Jan 25 '23 at 01:13
  • 2
    Debug mode is wrong for testing the efficiency of any code. See how the speed compares in a release build. – Drew Dormann Jan 25 '23 at 01:14
  • 3
    @RussellButler -- *just compiled it using debug mode in visual studio, so i don't know* -- Then the timings you are showing are meaningless. – PaulMcKenzie Jan 25 '23 at 01:14
  • 1
    a growth factor of 1.5 is popular – Neil Butterworth Jan 25 '23 at 01:14
  • thanks. i changed it to release mode and now the difference is much less (but the weiss implementation is still 2x faster, instead of 8x faster) – Russell Butler Jan 25 '23 at 01:15
  • yes, it turns out it was the resize factor. i changed mine to 1.25 and now they both run in the same amount of time. – Russell Butler Jan 25 '23 at 01:17
  • You may also see a difference if `Object` was non-trivial to construct. Right now, you are using `int`, but if it were `std::string` or similar, what woud the results be? Or how about `std::vector>`? I say this, because `std::vector` more than likely uses `placement-new`, and not "ordinary" `new` to create objects. – PaulMcKenzie Jan 25 '23 at 01:18
  • msvc uses a [growth rate of 1.5](https://github.com/microsoft/STL/blob/992efd9f9f96d1ed6f041332a4b1be1f31b8de02/stl/inc/vector#L1968), clang's libc++ [seems to use 2](https://github.com/llvm/llvm-project/blob/388b8c16c5610a54c639bb74e3c8de161e8ca1c6/libcxx/include/vector#LL984C55-L984C55), same for [gcc's libstc++](https://github.com/gcc-mirror/gcc/blob/b851ee9fdf0f3023635f0cb1f7c607b2d6801053/libstdc%2B%2B-v3/include/bits/stl_vector.h#L1896). What compiler are you using? – Chronial Jan 25 '23 at 01:19
  • 1
    To avoid resizing "penalty", if the fill size is known *a priori*, the `resize()` is your friend. – Eljay Jan 25 '23 at 01:20
  • 1
    @RussellButler [std::vector and Vector](https://godbolt.org/z/GKz8aP9q8). Note that `std::vector` is faster. – PaulMcKenzie Jan 25 '23 at 01:32
  • @PaulMcKenzie when i used a non-trivial object, you are correct, std::vector is faster. what do you mean about the difference between placement-new and ordinary new? – Russell Butler Jan 25 '23 at 03:24
  • @RussellButler [See this](https://stackoverflow.com/questions/222557/what-uses-are-there-for-placement-new). If you look at your compiler's implementation of `std::vector`, you will see placement-new being done, and not simply `new[]`. – PaulMcKenzie Jan 25 '23 at 08:14

2 Answers2

1

You compiled it in debug.

In debug, visual studio instruments std::vector with a bunch of debugging aids. It will detect in some cases if you try to use invalidated iterators, for example.

In general, doing performance testing in debug is useless. The only reason you should do it is if you are having problems where your debug build it too slow for development purposes.

Real use of your application should be done in release mode, which does optimization, and also removes extra "double check" work to help find bugs.

Your vector is indeed different, but that difference isn't that important at all.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
0

Your vector implementation and std::vector are fundamentally different.

Your vector default-constructs all values in the vector, up to reserve capacity, and push_back() merely replaces the next reserved value with the new value, using the assignment operator.

std::vector is fundamentally different,, and does not default-construct "non-existent" values in the vector, but constructs them "for real-sies". std::vector::push_back constructs the new value in the vector, your push_back assigns it.

Depending on the object in the container, its assignment operator and its constructor may have completely different logic, and comparative benchmarks are meaningless.

Sam Varshavchik
  • 114,536
  • 5
  • 94
  • 148