Benchmarking copy semantics vs. move semantics
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&amp; lst)
    {
        cout &lt;&lt; "Copy constructor" &lt;&lt; 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&amp; lst)
        : List()
    {
        cout &lt;&lt; "Copy constructor" &lt;&lt; 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&amp; 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&amp; lst)
        : List()
    {
        for (size_t i = 0; i != lst.sz; ++i)
        {
            push_back();
        }
    }
    List&amp; operator=(List const&amp; lst)
    {
        List tmp(lst);
        swap(tmp);

        return *this;
    }

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


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

    // Destroy
    ~List()
    {
        ListNode* next;
        for(auto tmp = head; tmp; tmp = next)
        {
            next = tmp-&gt;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-&gt;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&lt;std::chrono::milliseconds&gt;
            (m_clock.now().time_since_epoch()).count();
    }
};

List createList(CurrentTime&amp; 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 &lt;&lt; "Created Duration: " &lt;&lt; (end - start) &lt;&lt; " milliseconds." &lt;&lt; endl;

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

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

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

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


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

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

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

    cout &lt;&lt; "Move Assignment: " &lt;&lt; (end - start) &lt;&lt; " milliseconds." &lt;&lt; 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.