首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >基准数据复制

基准数据复制
EN

Code Review用户
提问于 2021-10-19 15:42:24
回答 4查看 1.2K关注 0票数 7

我受到最近一个问题的启发,对复制数据的不同方法做了一些基准测试,这就是我想出的:

代码语言:javascript
复制
#include <iostream>
#include <iomanip>
#include <chrono>
#include <vector>
#include <cstring>

class TimedTest
{
public:
    TimedTest(const std::string& name)
        : name{ name },
        time{ 0 },
        total{ 0 },
        testCount{ 0 }
    {
    }

    void run(int* dst, int* src, std::size_t count)
    {
        std::chrono::steady_clock::time_point begin = std::chrono::steady_clock::now();
        test(dst, src, count);
        std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();
        time = (double)std::chrono::duration_cast<std::chrono::milliseconds> (end - begin).count();
        total += time;
        testCount += 1.0;
    }
    virtual void test(int* dst, int* src, std::size_t count) = 0;

    double average()
    {
        return total / testCount;
    }

public:
    std::string name;
    double time;
    double total;
    double testCount;
};

class TimedTestMemCpy : public TimedTest
{
    using TimedTest::TimedTest;
    virtual void test(int* dst, int* src, std::size_t count) override
    {
        memcpy(dst, src, sizeof(int) * count);
    }
};

class TimedTestStdCopy : public TimedTest
{
    using TimedTest::TimedTest;
    virtual void test(int* dst, int* src, std::size_t count) override
    {
        std::copy(src, src + count, dst);
    }
};

class TimedTestSimpleLoop : public TimedTest
{
    using TimedTest::TimedTest;
    virtual void test(int* dst, int* src, std::size_t count) override
    {
        for (size_t i = 0; i < count; i++)
            dst[i] = src[i];
    }
};

class TimedTestPointerCopy : public TimedTest
{
    using TimedTest::TimedTest;
    virtual void test(int* dst, int* src, std::size_t count) override
    {
        int* end = dst + count; 
        while (dst != end)
            *dst++ = *src++;
    }
};

class TimedTestOMPCopy : public TimedTest
{
    using TimedTest::TimedTest;
    virtual void test(int* dst, int* src, std::size_t count) override
    {
#pragma omp parallel for 
        for (int i = 0; i < (int)count; i++)
            dst[i] = src[i];
    }
};

int main()
{
    constexpr std::size_t length = 200'000'000;
    int* src = new int[length];
    for (int i = 0; i < length; i++)
        src[i] = i;
    int* dst = new int[length];

    std::vector<TimedTest*> tests;
    tests.push_back(new TimedTestMemCpy("memcpy"));
    tests.push_back(new TimedTestStdCopy("std::copy"));
    tests.push_back(new TimedTestSimpleLoop("simpleLoop"));
    tests.push_back(new TimedTestPointerCopy("pointerCopy"));
    tests.push_back(new TimedTestOMPCopy("OMPCopy"));

    std::cout << std::setw(5) << "Test#";
    for (auto test : tests)
        std::cout << std::setw(12) << test->name << std::setw(9) << "Avg";
    std::cout << "\n";

    for (int i = 0; i < 100; i++)
    {
        std::cout << std::setw(5) << i;
        for (auto test : tests)
        {
            test->run(dst, src, length);
            std::cout << std::setw(12) << test->time << std::setw(9) << test->average();
        }
        std::cout << "\n";
    }

    for (auto test : tests)
        delete test;

    delete[] src;
    delete[] dst;
}

我希望对改进基准/一般代码的结果或建议提出任何意见。

EN

回答 4

Code Review用户

回答已采纳

发布于 2021-10-19 17:59:17

避免newdelete

通常不需要在newdelete中手动使用C++,容器可以自己管理内存,而且在几乎所有其他情况下都可以使用std::unique_ptr。在您的程序中,srcdst可以制作为std::vectors:

代码语言:javascript
复制
std::vector<int> src(length);
std::vector<int> dst(length);

对于测试用例的向量,可以像这样使用std::unique_ptr

代码语言:javascript
复制
std::vector<std::unique_ptr<TimedTest>> tests;
tests.push_back(std::make_unique<TimedTestMemcpy>("memcpy"));
...

使用组合而不是继承

使用继承的方法的问题是,您必须为每个测试用例创建一个新的class。他们唯一增加的就是一个函数。与继承不同,您可以使TimedTest成为一个非虚拟类,并将函数存储为std::function类型的成员变量:

代码语言:javascript
复制
class TimedTest
{
    // A type alias to avoid repeating the full type
    using Function = std::function<void(int*, int*, std::size_t)>;

public:
    TimedTest(const std::string& name, Function test)
        : name{name}, test{test}
    {
    }
...
private:
    std::string name;
    Function test;
}

请注意,您的run()函数根本不必更改!由于这个类现在不再是虚拟的,所以不需要将指向它的指针存储在向量tests中,而是可以写:

代码语言:javascript
复制
static void test_memcpy(int* src, int* dst, std::size_t count) {
    memcpy(dst, src, sizeof(*dst) * count);
}

...

std::vector<TimedTest> tests = {
    {"memcpy", test_memcpy},
    ...
};

避免在所有

中使用类

更好的是,正如user673679JDługosz所提到的,不需要有一个class TimedTest,您只需编写一个函数,该函数将另一个函数作为参数并在循环中运行。与您的代码唯一的区别是,您将运行每个测试一次,并累积结果,并在最后计算所有的平均值。

使用autousing避免编写长类型名称

与其在这一行代码中写出完整的类型名称,不如:

代码语言:javascript
复制
std::chrono::steady_clock::time_point begin = std::chrono::steady_clock::now();

我建议您使用using为要使用的时钟类型创建类型别名,并使用auto来避免在适当的情况下指定类型。然后,上述内容成为:

代码语言:javascript
复制
using clock = std::chrono::steady_clock;
auto start = clock::now();
...
auto stop = clock::now();
票数 12
EN

Code Review用户

发布于 2021-10-19 17:59:03

避免手动内存管理

代码语言:javascript
复制
int* src = new int[length];
for (int i = 0; i < length; i++)
    src[i] = i;
int* dst = new int[length];

为此,我们可以使用两个std::vector<int>s,避免手动内存管理。可以使用.data()成员函数获得指向底层存储的指针。

代码语言:javascript
复制
std::vector<TimedTest*> tests;

同样,我们应该在这里使用std::vector<std::unique_ptr<TimedTest>>,这样我们就不必手动地使用delete了。

添加虚拟析构函数

我们通过基类指针(TimedTest*)删除类,因此我们必须有一个virtual析构函数来确保正确的清理。

使用std::iota

代码语言:javascript
复制
for (int i = 0; i < length; i++)
    src[i] = i;

为此,我们可以在std::iota头中使用<numeric>

保持定时精度

代码语言:javascript
复制
(double)std::chrono::duration_cast<std::chrono::milliseconds> (end - begin).count();

std::chrono::milliseconds是一个整数类型。所以这样做就失去了很多准确性。

我们应该在std::chrono::steady_clock::duration (可能是纳秒)成员中积累时间,然后在所有测试运行完成后进行毫秒(或任何其他)的转换。

如果我们想在最后输出一个浮点结果,我们需要在最后一次使用类似于:std::chrono::duration_cast<std::chrono::milliseconds<double>>(...).count()之类的东西。

简化

代码语言:javascript
复制
class TimedTest;

这门课(和它的孩子)并不是真正必要的。计时成员在测试运行之前不需要存在,并且在输出后不再使用。它们应该是测试运行循环中的局部变量。

对每个virtual调用使用一个test函数也会增加测试函数的开销。相反,我们可以编写一个模板函数来运行测试的设定次数并返回时间。也许是这样的:

代码语言:javascript
复制
template<class F>
std::chrono::steady_clock::duration run_test(std::size_t num_runs, F test_func)
{
    auto begin = std::chrono::steady_clock::now();
    
    for (auto i = std::size_t { 0 }; i != num_runs; ++i)
        test_func();
    
    auto end = std::chrono::steady_clock::now();
    
    return (end - begin);
}

这可以用这样的lambda调用:run_test(100, [&] () { test_memcpy(src.data(), dst.data(), length); }),这对于编译器来说应该很容易优化。

misc.其他事情:

  • 在C++中,我们应该从技术上使用std::memcpy,而不仅仅是memcpy (因为<cstring>头在那里声明它,而另一个则可能不存在)。
  • 它可能会影响在不同测试之间共享源和目标内存的时间。每个人都应该进行自己的分配和清理(在代码的定时部分之外)。
  • 如果我们不想使用自己的工具,我们可以使用谷歌工作台来进行这类测量,它有一个在线平台这里
票数 8
EN

Code Review用户

发布于 2021-10-19 17:59:14

我觉得你的方法太复杂了。您不需要创建/释放对象并维护对象集合。您只有具有相同签名的各种不同实现。因此,给每个实现一个不同的函数名,并编写:

代码语言:javascript
复制
test ("memcpy", &TestMemCpy);
test ("simple loop", &TestSimpleLoop);
   // etc.

这就产生了其他问题,比如使用裸露的new并必须显式地编写delete循环,就这样离开了。

同样,不需要在堆上分配srcdst内存。它可能只是声明为数组的全局变量。

我担心基准不会将事情的时间安排到必要的精确程度,从而注意到任何有用的东西。至少,只做一次测试可能会出现一个未知的错误或抖动,在典型的时间,你不会知道。通常的基准测试框架会运行很多次。您正在运行所有测试100次,而不是在进入下一个测试之前运行每个测试100次。后者是更好的,因为代码缓存问题。

关于内存复制的

,特别是

正如评论中的对话所指出的那样,基准内存复制尤其存在与基准测试框架无关的问题。

看看编译器资源管理器下的函数,打开了优化。您可以逐个注释掉push_back行和取消注释,以清楚地看到每个选项生成的代码。

编译器为所有这些生成几乎相同的代码!内存复制是它所关注的东西,一旦编译器理解了您的意思,它就会抛出您所写的内容,并为该结果放入最佳代码。“优化”是基于您可以用来控制它的各种标志,包括目标体系结构。

有了这样的编译器,您应该专注于清楚地表达您的意图,而不是想象汇编语言是您所写内容的粗略的逐行翻译。

这意味着使用memcpystd::copy。因为这些都是标准的,所以编译器可以对它们所做的事情进行深入的理解.在实际代码中,您使用的是类型化对象而不是原始字节,所以始终使用std::copy。注意,std::copy和相关函数也可以被赋予一个执行策略参数,因此它可以像上一个示例那样并行化。

票数 6
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/269140

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档