C++11的移动语义和指针之间的性能测量

11

针对我的应用场景,我需要非常快速地插入和删除数据包列表中的数据。

我认为有两种常见的解决方法:

  • 通过插入/删除指向这些数据包的指针
  • 使用移动语义来插入/删除副本

无论如何,我认为采用指针的解决方案应该是最有效的,即使有自己的垃圾回收的缺点。

我实现了3个测试用例以进行更好的比较:

  • 使用副本
  • 使用移动语义
  • 使用指针

为了测量每个测试用例所花费的时间,我还实现了一个计时器类:

class HighPerformanceTimer {

public:
    enum TimerResolution {
        SECONDS      = 1,
        MILLISECONDS = SECONDS * 1000,
        MICROSECONDS = MILLISECONDS * 1000,
        NANOSECONDS  = MICROSECONDS * 1000
    };


    explicit HighPerformanceTimer(const TimerResolution resolution = NANOSECONDS)
        : m_resolution(resolution)
        , m_frequency(0.0)
        , m_startTime({0})
        , m_stopTime({0}) {}
    ~HighPerformanceTimer(void) {}


    bool Init(void) {
        LARGE_INTEGER frequency;
        if(0 == ::QueryPerformanceFrequency(&frequency)) {
            return false;
        }

        /* Check for zero divisor. */
        if(0 == frequency.QuadPart) {
            return false;
        }

        /* Change frequency to double for internal timer resolution. */
        switch(m_resolution) {
            case NANOSECONDS:
                m_frequency = frequency.QuadPart / (NANOSECONDS * 1.0);
                break;

            case MICROSECONDS:
                m_frequency = frequency.QuadPart / (MICROSECONDS * 1.0);
                break;

            case MILLISECONDS:
                m_frequency = frequency.QuadPart / (MILLISECONDS * 1.0);
                break;

            default:
                /**
                 * SECONDS
                 * m_frequency has a resolution in seconds by default
                 */
                m_frequency = frequency.QuadPart * 1.0;
                break;
        }

        return true;
    }

    void Start(void) {
        ::QueryPerformanceCounter(&m_startTime);
    }

    void Stop(double& intervall) {
        ::QueryPerformanceCounter(&m_stopTime);
        intervall = ((m_stopTime.QuadPart - m_startTime.QuadPart) / m_frequency);
    }

private:
    const TimerResolution     m_resolution;
    double                    m_frequency;
    LARGE_INTEGER             m_startTime;
    LARGE_INTEGER             m_stopTime;
    CRITICAL_SECTION          m_timerLock;

};

以下是我的主要类:

class Packet {

public:
    Packet(const uint8* data, uint16 length) {
        if(0 != length) {
            if(nullptr == data) {
                m_data = std::vector<uint8>(length, 0x00);
            } else {
                m_data.assign(data, data + length);
            }
        }
    }

    Packet(const Packet& rhs) : m_data(rhs.m_data) {}

    Packet& operator=(const Packet& rhs) {
        m_data = rhs.m_data;
        return *this;
    }

    Packet(const Packet&& rhs) : m_data(rhs.m_data) {}

    Packet& operator=(const Packet&& rhs) {
        m_data = rhs.m_data;
        return *this;
    }

    std::vector<uint8> m_data;

};

void Measurement_1(void);
void Measurement_2(void);
void Measurement_3(void);

constexpr uint16 payloadLength = 15000;
uint8 payload[payloadLength];

/* Initialize high performance timer. */
HighPerformanceTimer hpt(HighPerformanceTimer::MICROSECONDS);

int main(void) {
    hpt.Init();

    /* Fill packet data. */
    for(unsigned int j = 0; j < payloadLength; ++j) {
        payload[j] = 0xFF;
    }


    Measurement_1();
    Measurement_2();
    Measurement_3();

    return EXIT_SUCCESS;
}

void Measurement_1(void) {
    /* Measurement with copies. */

    double result[25];

    for(unsigned int k = 0; k < 25; ++k) {
        /* Start measurement. */
        double timeElapsed = 0.0;
        std::list<Packet> mylist;
        hpt.Start();
        /* Begin insertion. */
        for(unsigned int i = 0; i < 1000; ++i) {
            Packet f(payload, payloadLength);
            mylist.push_back(f);
        }
        /* End insertion. */

        /* Begin removal. */
        for(unsigned int i = 0; i < 1000; ++i) {
            Packet f = mylist.front();
            mylist.pop_front();
        }
        /* End removal. */
        hpt.Stop(timeElapsed);
        result[k] = timeElapsed;
        /* Stop measurement. */
    }

    for(unsigned int i = 0; i < 25; ++i) {
        std::cout << "with copies: " << std::setprecision(3) << std::fixed << result[i] << std::endl;
    }
}

void Measurement_2(void) {
    /* Measurement with move semantics. */

    double result[25];

    for(unsigned int k = 0; k < 25; ++k) {
        /* Start measurement. */
        double timeElapsed = 0.0;
        std::list<Packet> mylist;
        hpt.Start();
        /* Begin insertion. */
        for(unsigned int i = 0; i < 1000; ++i) {
            Packet f(payload, payloadLength);
            mylist.push_back(std::move(f));
        }
        /* End insertion. */

        /* Begin removal. */
        for(unsigned int i = 0; i < 1000; ++i) {
            Packet f = std::move(mylist.front());
            mylist.pop_front();
        }
        /* End removal. */
        hpt.Stop(timeElapsed);
        result[k] = timeElapsed;
        /* Stop measurement. */
    }

    for(unsigned int i = 0; i < 25; ++i) {
        std::cout << "with moves: " << std::setprecision(3) << std::fixed << result[i] << std::endl;
    }
}

void Measurement_3(void) {
    /* Measurement with pointers. */

    double result[25];

    for(unsigned int k = 0; k < 25; ++k) {
        /* Start measurement. */
        double timeElapsed = 0.0;
        std::list<Packet*> mylist;
        hpt.Start();
        /* Begin insertion. */
        for(unsigned int i = 0; i < 1000; ++i) {
            mylist.push_back(new Packet(payload, payloadLength));
        }
        /* End insertion. */

        /* Begin removal. */
        Packet* f = nullptr;
        for(unsigned int i = 0; i < 1000; ++i) {
            f = mylist.front();
            if(nullptr != f) {
                mylist.pop_front();
                delete f;
            }
        }
        /* End removal. */
        hpt.Stop(timeElapsed);
        result[k] = timeElapsed;
        /* Stop measurement. */
    }

    for(unsigned int i = 0; i < 25; ++i) {
        std::cout << "with pointers: " << std::setprecision(3) << std::fixed << result[i] << std::endl;
    }
}

目前我遇到的问题是,我得到的指针版本、移动语义版本和复制版本几乎得出了相同的测量结果。

我哪里做错了吗?

问候!


1
如果您运行每个测试多次(考虑几十万次),并选择最快的时间(或前10%),则可以获得更可靠和准确的结果。这将减少运行之间的抖动,减少操作系统调度程序在测试中间交换您的进程的影响,并预热CPU缓存。 - Cameron
对于@Cameron所说的话,我想再补充一点:每个Measurement_X方法调用应该至少需要几秒钟(最好是几十秒钟)。 - Constructor
6
Packet(const Packet&& rhs) : m_data(rhs.m_data) {} 这个语句无法调用移动语义。请尝试使用 Packet(Packet&& rhs) noexcept - dyp
1
你的代码有误。正如@DyP所提到的,&&运算符不应该有const参数。它们应该复制指针并在另一个实例中将它们清零。首先编写正确的代码,并确保调用了&&运算符。然后测量性能。 - Alex F
4
哦,不要忘记对参数使用std::movePacket(Packet&& rhs) noexcept : m_data( std::move(rhs.m_data) ) {} - dyp
就是这样。我自己没找到错误... - Steve Murdock
1个回答

4
移动构造函数实际上是复制另一个向量。它调用了向量的复制构造函数。
这里有一个修复方法:
Packet(Packet&& rhs) : m_data(std::move(rhs.m_data)) {}

关于时间,我的PC上指针是最快的。
- 指针:平均5500 - 修复前移动:平均8100 - 修复后移动:平均5500 - 复制:平均8700 结果相当一致(以微秒为单位)。
编辑: 在修复后,足够运行的移动操作符显示出与指针时间类似的性能。增加测试时间显示出类似的比例。
调试模式下的结果:
- 指针:平均16500 - 修复后移动:平均19000 - 复制:平均30000 在调试构建中,指针更快,可能是由于实现方式(调用了多少内联函数)。

不要忘记使用 noexcept - user1508519
你可以尝试使用 emplace_back 而不是 push_back 吗? - dyp
2
emplace_backnoexcept 没有改变任何东西(性能) - egur
只是想留下一条评论,不知道指针的速度从哪里来;)干得好。 - dyp
太好了!这些结果符合我对指针性能的期望。 也许我以后会再尝试新的测量方法。 到目前为止,非常感谢! - Steve Murdock
在我的理解中,移动构造函数基本上在旧对象和新对象之间在幕后复制指针。自定义移动构造函数确实做到了这一点,不是吗?因此,性能应该非常接近。虽然不完全相等,因为单个对象将为其成员调用多个移动操作。 - Vassilis

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接