我受到最近一个问题的启发,对复制数据的不同方法做了一些基准测试,这就是我想出的:
#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;
}我希望对改进基准/一般代码的结果或建议提出任何意见。
发布于 2021-10-19 17:59:17
new和delete通常不需要在new和delete中手动使用C++,容器可以自己管理内存,而且在几乎所有其他情况下都可以使用std::unique_ptr。在您的程序中,src和dst可以制作为std::vectors:
std::vector<int> src(length);
std::vector<int> dst(length);对于测试用例的向量,可以像这样使用std::unique_ptr:
std::vector<std::unique_ptr<TimedTest>> tests;
tests.push_back(std::make_unique<TimedTestMemcpy>("memcpy"));
...使用继承的方法的问题是,您必须为每个测试用例创建一个新的class。他们唯一增加的就是一个函数。与继承不同,您可以使TimedTest成为一个非虚拟类,并将函数存储为std::function类型的成员变量:
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中,而是可以写:
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},
...
};中使用类
更好的是,正如user673679和JDługosz所提到的,不需要有一个class TimedTest,您只需编写一个函数,该函数将另一个函数作为参数并在循环中运行。与您的代码唯一的区别是,您将运行每个测试一次,并累积结果,并在最后计算所有的平均值。
auto和using避免编写长类型名称与其在这一行代码中写出完整的类型名称,不如:
std::chrono::steady_clock::time_point begin = std::chrono::steady_clock::now();我建议您使用using为要使用的时钟类型创建类型别名,并使用auto来避免在适当的情况下指定类型。然后,上述内容成为:
using clock = std::chrono::steady_clock;
auto start = clock::now();
...
auto stop = clock::now();发布于 2021-10-19 17:59:03
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()成员函数获得指向底层存储的指针。
std::vector<TimedTest*> tests;同样,我们应该在这里使用std::vector<std::unique_ptr<TimedTest>>,这样我们就不必手动地使用delete了。
我们通过基类指针(TimedTest*)删除类,因此我们必须有一个virtual析构函数来确保正确的清理。
for (int i = 0; i < length; i++)
src[i] = i;为此,我们可以在std::iota头中使用<numeric>。
(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()之类的东西。
class TimedTest;这门课(和它的孩子)并不是真正必要的。计时成员在测试运行之前不需要存在,并且在输出后不再使用。它们应该是测试运行循环中的局部变量。
对每个virtual调用使用一个test函数也会增加测试函数的开销。相反,我们可以编写一个模板函数来运行测试的设定次数并返回时间。也许是这样的:
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); }),这对于编译器来说应该很容易优化。
发布于 2021-10-19 17:59:14
我觉得你的方法太复杂了。您不需要创建/释放对象并维护对象集合。您只有具有相同签名的各种不同实现。因此,给每个实现一个不同的函数名,并编写:
test ("memcpy", &TestMemCpy);
test ("simple loop", &TestSimpleLoop);
// etc.这就产生了其他问题,比如使用裸露的new并必须显式地编写delete循环,就这样离开了。
同样,不需要在堆上分配src和dst内存。它可能只是声明为数组的全局变量。
我担心基准不会将事情的时间安排到必要的精确程度,从而注意到任何有用的东西。至少,只做一次测试可能会出现一个未知的错误或抖动,在典型的时间,你不会知道。通常的基准测试框架会运行很多次。您正在运行所有测试100次,而不是在进入下一个测试之前运行每个测试100次。后者是更好的,因为代码缓存问题。
关于内存复制的
正如评论中的对话所指出的那样,基准内存复制尤其存在与基准测试框架无关的问题。
看看编译器资源管理器下的函数,打开了优化。您可以逐个注释掉push_back行和取消注释,以清楚地看到每个选项生成的代码。
编译器为所有这些生成几乎相同的代码!内存复制是它所关注的东西,一旦编译器理解了您的意思,它就会抛出您所写的内容,并为该结果放入最佳代码。“优化”是基于您可以用来控制它的各种标志,包括目标体系结构。
有了这样的编译器,您应该专注于清楚地表达您的意图,而不是想象汇编语言是您所写内容的粗略的逐行翻译。
这意味着使用memcpy或std::copy。因为这些都是标准的,所以编译器可以对它们所做的事情进行深入的理解.在实际代码中,您使用的是类型化对象而不是原始字节,所以始终使用std::copy。注意,std::copy和相关函数也可以被赋予一个执行策略参数,因此它可以像上一个示例那样并行化。
https://codereview.stackexchange.com/questions/269140
复制相似问题