1
\$\begingroup\$

I am still in the process of learning modern C++, and decided to write this dummy program for actually benchmarking copy semantics vs. move semantics:

#include <chrono>
#include <iostream>
#include <cstdint>

using std::cout;
using std::endl;

class List {
public:
    void push_back();
    List() : sz{0}, head{nullptr}, tail{nullptr} { }

    List(List&& lst) : head{lst.head}, tail{lst.tail}, sz{lst.sz} {
        cout << "Move constructor" << endl;
        lst.head = nullptr;
        lst.tail = nullptr;
        lst.sz = 0;
    }

    List(const List& lst)
    {
        cout << "Copy constructor" << endl;
        for (size_t i = 0; i != lst.sz; ++i)
        {
            push_back();
        }
    }

    List& operator=(List&& lst)
    {
        cout << "Move assignment" << endl;
        head = lst.head;
        tail = lst.tail;
        sz = lst.sz;
        lst.head = nullptr;
        lst.tail = nullptr;
        lst.sz = 0;
        return *this;
    }

    List& operator=(const List& lst)
    {
        cout << "Copy assignment" << endl;
        head = nullptr;
        tail = nullptr;
        sz = 0;

        for (size_t i = 0; i != lst.sz; ++i)
        {
            push_back();
        }

        return *this;
    }

private:

    struct ListNode {
        ListNode* next;
    };

    ListNode* head;
    ListNode* tail;
    size_t sz;
};

void List::push_back()
{
    ListNode* new_node = new ListNode;
    new_node->next = nullptr;

    if (head == nullptr)
    {
        head = new_node;
    }
    else
    {
        tail->next = new_node;
    }

    tail = new_node;
    ++sz;
}

class CurrentTime {
    std::chrono::high_resolution_clock m_clock;

public:
    uint64_t milliseconds()
    {
        return std::chrono::duration_cast<std::chrono::milliseconds>
            (m_clock.now().time_since_epoch()).count();
    }
};

List createList(CurrentTime& ct)
{
    List lst;
    constexpr size_t HUNDRED_MILLION = 100 * 1000 * 1000;

    auto start = ct.milliseconds();
    for (size_t i = 0; i != HUNDRED_MILLION; ++i)
    {
        lst.push_back();
    }
    auto end = ct.milliseconds();

    cout << "createList in " << (end - start) << " milliseconds." << endl;

    return lst;
}

int main() {
    CurrentTime ct;

    auto start = ct.milliseconds();
    List list;
    list = createList(ct);
    auto end = ct.milliseconds();

    cout << "Duration: " << (end - start) << " milliseconds." << endl;

    start = ct.milliseconds();
    List list2(list);
    end = ct.milliseconds();

    cout << "Duration: " << (end - start) << " milliseconds." << endl;

    start = ct.milliseconds();
    List list3(std::move(list));
    end = ct.milliseconds();

    cout << "Duration: " << (end - start) << " milliseconds." << endl;
}

Did I manage to capture the idiomatic use of move facilities in this demonstration program? Tell me anything that comes to mind.

\$\endgroup\$
2
\$\begingroup\$

OK if we are going to do some timing then lets fix a few things.

When I compile this with optimizations -O3 it crashes. So getting timmings will be hard.

So lets fix the copy constructor.

Member variables are initialized depending on the context.

List a;          // Members are default initialized.
List a = List(); // Members are zero    initialized.

If the members are default initialized then POD members are left in an undefined state. So the following constructor:

    List(const List& lst)
    {
        cout << "Copy constructor" << endl;
        for (size_t i = 0; i != lst.sz; ++i)
        {
            push_back();
        }
    }

leaves all its members uninitialized (and thus in an undefined state) when used like this:

List list2(list);

Thus the call th push_back() is going to start manipulating some undefined pointers.

Fix by either explicitly initializing all members in the constructor or by chaining a constructor that will do it for you. Sinve your default constructor initializes the members correctly I will chain that.

    List(const List& lst)
        : List()
    {
        cout << "Copy constructor" << endl;
        for (size_t i = 0; i != lst.sz; ++i)
        {
            push_back();
        }
    }

IO and timing do not work well.

The IO operations are notoriously complex and using IO can change a lot. You want to avoid all IO operation during any timing phase and print the results out at the end after all timing information has been generated.

Standard Idioms

There are a couple standard idioms you should follow (as most code will be written like this).

Rule of Five. You forgot a destructor. Though it will not affect your timmings you should probably write one just to be tidy.

Copy and Swap. The copy assignment operator is usually written in terms of copy and swap.

Swap on move. Movement is usually implemented as a swap operation. That way you don't have to explicitly tidy up your content at this point if you don't need to. But the moved from object will do the tidy up eventually if it needs to. Currently your move assignment will leak if used.

Noexcept: The move operators should be marked as no except. This allows containers they are used within to perform certain operations that would not otherwise be allowed to maintain the strong exception guarantee.

class List {
public:
    void push_back();
    void swap(List& other) noexcept
    {
        using std::swap;
        swap(sz,   other.sz);
        swap(head, other.head);
        swap(tail, other.tail);
    }
    List()
       : head{nullptr}
       , tail{nullptr}
       , sz{0}
    {}

    // Copy    
    List(List const& lst)
        : List()
    {
        for (size_t i = 0; i != lst.sz; ++i)
        {
            push_back();
        }
    }
    List& operator=(List const& lst)
    {
        List tmp(lst);
        swap(tmp);

        return *this;
    }

    // Move
    List(List&& lst) noexcept
       : List()
    {
        swap(lst);
    }


    List& operator=(List&& lst)
    {
        swap(lst);
        return *this;
    }

    // Destroy
    ~List()
    {
        ListNode* next;
        for(auto tmp = head; tmp; tmp = next)
        {
            next = tmp->next;
            delete tmp;
        }
    }
private:

    struct ListNode {
        ListNode* next;
    };

    ListNode* head;
    ListNode* tail;
    size_t sz;
};

void List::push_back()
{
    ListNode* new_node = new ListNode{nullptr};

    if (head == nullptr)
    {
        head = new_node;
    }
    else
    {
        tail->next = new_node;
    }

    tail = new_node;
    ++sz;
}

class CurrentTime {
    std::chrono::high_resolution_clock m_clock;

public:
    uint64_t milliseconds()
    {
        return std::chrono::duration_cast<std::chrono::milliseconds>
            (m_clock.now().time_since_epoch()).count();
    }
};

List createList(CurrentTime& ct)
{
    List lst;
    constexpr size_t HUNDRED_MILLION = 100 * 1000 * 1000;

    for (size_t i = 0; i != HUNDRED_MILLION; ++i)
    {
        lst.push_back();
    }

    return lst;
}

int main() {
    CurrentTime ct;

    auto start = ct.milliseconds();
    List list = createList(ct);
    auto end = ct.milliseconds();

    cout << "Created Duration: " << (end - start) << " milliseconds." << endl;

    start = ct.milliseconds();
    List list2(list);
    end = ct.milliseconds();

    cout << "Copy Duration: " << (end - start) << " milliseconds." << endl;

    start = ct.milliseconds();
    List list3(std::move(list));
    end = ct.milliseconds();

    cout << "Move Duration: " << (end - start) << " milliseconds." << endl;


    start = ct.milliseconds();
    list = list2;
    end = ct.milliseconds();

    cout << "Copy Assignment: " << (end - start) << " milliseconds." << endl;

    start = ct.milliseconds();
    list = std::move(list3);
    end = ct.milliseconds();

    cout << "Move Assignment: " << (end - start) << " milliseconds." << endl;

}

Note: Your attempt at measuring move assignment was wrong. You were measuring the time required to create the list then move it. So I removed that output.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.