【问题标题】:Why is memcpy slower than a reinterpret_cast when parsing binary data?为什么解析二进制数据时 memcpy 比 reinterpret_cast 慢?
【发布时间】:2012-10-29 07:03:04
【问题描述】:

TLDR:我忘记启用编译器优化。启用优化后,性能(几乎)相同。


原帖

从二进制数据中读取整数时,我注意到 memcpy 比强制转换解决方案慢。

版本 1:reinterpret_cast,由于潜在的对齐问题而异味,但也更快(?)

int get_int_v1(const char * data) { return *reinterpret_cast<const int*>(data); }

版本 2:memcpy,正确但速度稍慢:

int get_int_v2(const char * data) { int result; memcpy(&result, data, sizeof(result)); return result; }

我有a benchmark on Ideone

供以后参考,代码为:

#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <iostream>
#include <vector>
#include <sys/time.h>

double get_current_time()
{
    timeval tv;
    gettimeofday(&tv, NULL);
    return double (tv.tv_sec) + 0.000001 * tv.tv_usec;
}

int get_int_v1(const char * data) { return *reinterpret_cast<const int*>(data); }
int get_int_v2(const char * data) { int result; memcpy(&result, data, sizeof(result)); return result; }

const unsigned iterations = 200 * 1000 * 1000;

double test_v1(const char * c, unsigned & prevent_optimization)
{
    double start = get_current_time();
    for (unsigned i = 0; i != iterations; ++i)
    {
        prevent_optimization += get_int_v1(c);
    }
    return get_current_time() - start;
}

double test_v2(const char * c, unsigned & prevent_optimization)
{
    double start = get_current_time();
    for (unsigned i = 0; i != iterations; ++i)
    {
        prevent_optimization += get_int_v2(c);
    }
    return get_current_time() - start;
}

int main()
{
    srand(time(0));

    // Initialize data
    std::vector<int> numbers(1000);
    for (std::vector<int>::size_type i = 0; i != numbers.size(); ++i)
    {
        numbers[i] = i;
    }

    // Repeat benchmark 4 times.
    for (unsigned i = 0; i != 4; ++i)
    {
        unsigned p = 0;
        std::vector<int>::size_type index = rand() % numbers.size();
        const char * c = reinterpret_cast<const char *>(&numbers[index]);    
        std::cout << "v1: " << test_v1(c, p) << std::endl;
        std::cout << "v2: " << test_v2(c, p) << std::endl << std::endl;
    }
}

结果是:

v1: 0.176457
v2: 0.557588

v1: 0.17654
v2: 0.220581

v1: 0.176826
v2: 0.22012

v1: 0.176131
v2: 0.220633

我的问题是:

  • 我的基准测试是否正确?
  • 如果是,那么为什么 v2(使用 memcpy)会变慢?由于两个版本都返回数据的副本,我认为性能应该没有差异。
  • 我怎样才能实施正确且快速的解决方案?


更新

我太傻了,忘了考虑 Ideone 不执行编译器优化。我还稍微调整了代码并得出以下结论:

#include <algorithm>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <iomanip> 
#include <iostream> 
#include <vector>
#include <sys/time.h>

double get_current_time()
{
    timeval tv;
    gettimeofday(&tv, NULL);
    return double (tv.tv_sec) + 0.000001 * tv.tv_usec;
}

struct test_cast
{
    int operator()(const char * data) const 
    {
        return *((int*)data);
    }
};

struct test_memcpy
{
    int operator()(const char * data) const 
    {
        int result;
        memcpy(&result, data, sizeof(result));
        return result;
    }
};

struct test_std_copy
{
    int operator()(const char * data) const 
    {
        int result;
        std::copy(data, data + sizeof(int), reinterpret_cast<char *>(&result));
        return result;
    }
};

enum
{
    iterations = 2000,
    container_size = 2000
};

std::vector<int> get_random_numbers()
{
    std::vector<int> numbers(container_size);
    for (std::vector<int>::size_type i = 0; i != numbers.size(); ++i)
    {
        numbers[i] = rand();
    }
    return numbers;
}

std::vector<int> get_random_indices()
{
    std::vector<int> numbers(container_size);
    for (std::vector<int>::size_type i = 0; i != numbers.size(); ++i)
    {
        numbers[i] = i;
    }
    std::random_shuffle(numbers.begin(), numbers.end());
    return numbers;
}

template<typename Function>
unsigned benchmark(const Function & f, unsigned & counter)
{
    std::vector<int> container = get_random_numbers();
    std::vector<int> indices = get_random_indices();
    double start = get_current_time();
    for (unsigned iter = 0; iter != iterations; ++iter)
    {
        for (unsigned i = 0; i != container.size(); ++i)
        {
            counter += f(reinterpret_cast<const char*>(&container[indices[i]]));
        }
    }
    return unsigned(0.5 + 1000.0 * (get_current_time() - start));
}

int main()
{
    srand(time(0));
    unsigned counter = 0;

    std::cout << "cast:      " << benchmark(test_cast(),     counter) << " ms" << std::endl;
    std::cout << "memcpy:    " << benchmark(test_memcpy(),   counter) << " ms" << std::endl;
    std::cout << "std::copy: " << benchmark(test_std_copy(), counter) << " ms" << std::endl;
    std::cout << "(counter:  " << counter << ")" << std::endl << std::endl;

}

结果现在几乎相等(std::copy 除外,由于某种原因速度较慢):

g++ -o test -O0 -Wall -Werror -Wextra -pedantic-errors main.cpp
cast:      56 ms
memcpy:    60 ms
std::copy: 290 ms
(counter:  2854155632)

g++ -o test -O1 -Wall -Werror -Wextra -pedantic-errors main.cpp
cast:      9 ms
memcpy:    14 ms
std::copy: 20 ms
(counter:  3524665968)

g++ -o test -O2 -Wall -Werror -Wextra -pedantic-errors main.cpp
cast:      4 ms
memcpy:    5 ms
std::copy: 20 ms
(counter:  2590914608)

g++ -o test -O3 -Wall -Werror -Wextra -pedantic-errors main.cpp
cast:      4 ms
memcpy:    5 ms
std::copy: 18 ms
(counter:  2590914608)

【问题讨论】:

  • 好吧,因为 reinterpret_cast 什么都不做(它只是对编译器的提示,是 O(0),但 memcpy 遍历数据,它是 O(n)
  • 你检查生成的代码了吗?我敢打赌它不一样。
  • 然而,reinterpret_cast 是可疑的。我可以更改我的代码以使我有速度正确性吗?

标签: c++ performance


【解决方案1】:

您需要查看发出的代码。显然,优化器“应该”能够将 memcpy 转换为单个可能未对齐的 int 大小的读入返回值,但如果您看到不同的时间,那么我认为 x86 意味着它没有。

在我的机器上,使用带有-O2 的 gcc,我一直得到 0.09。使用-O3,我总是得到 0(我没有检查这是否比时间粒度更快,或者优化器已经删除了你的所有代码)。

很可能,答案只是您没有使用正确的编译器标志(或者 ideone 没有)。

在潜在未对齐读取需要与对齐读取不同指令的架构上,reinterpret_cast 可能会发出对齐读取,而memcpy 可能必须发出未对齐读取(取决于函数的调用方式 - - 在这种情况下,数据实际上是对齐的,但我不知道编译器在什么条件下可以证明这一点)。在这种情况下,我希望reinterpret_cast 代码可能比memcpy 更快,但如果有人传入未对齐的指针,这当然是不正确的。

【讨论】:

  • 我在使用 -O3 时也得到了 0 次。我还怀疑编译器优化了 everything 了。我很快就会更新我的程序,但目前工作有点忙:)
  • @StackedCrooked: 是的,你的prevent_optimization 是一个勇敢的努力,但是一旦函数被内联,优化器仍然有机会注意到它没有被使用并忽略它。您可以在测试结束时打印它的值。
【解决方案2】:

强制转换是编译时操作,而memcpy()运行时操作。这就是铸造对运行时间没有影响的原因。

【讨论】:

  • 是的,差不多。也许更好地解释一下:它是一个编译时only操作(一些编译时的东西确实对运行时间有影响,例如优化)。
  • 在这两种情况下,实际副本都是在运行时制作的。我看不出 reinterpret_cast 如何从编译时优化中受益。
  • @StackedCrooked 在您的第一个函数中,您每次只复制一个 int 值(在函数返回时)。在你的第二个函数中,你在函数返回和memmcpy(+ func 调用的开销等)中执行它
  • 这样回答问题的标题,但你真的看过get_int_v1get_int_v2的代码吗?
  • @SteveJessop 如果您在谈论取消引用强制转换结果,我实际上想到了它,但由于它是一个右值,编译器几乎肯定会优化它并防止不必要的复制。但是,为了完整起见,您的评论应包含在答案中。
【解决方案3】:

memcpy 无法复制到寄存器,它会进行内存到内存的复制。 get_int_v1 中的 reinterpret_cast 可以更改寄存器中保存的指针类型,甚至不需要寄存器到寄存器的副本。

【讨论】:

  • 是的,虽然我已经看到编译器在简单的情况下完全优化了 memcpy
  • GCC 确实为小型固定大小的副本可靠地优化了 memcpy,尤其是当大小为 sizeof(int) 时。例如在加载指令没有对齐要求的机器上,它将可靠地将memcpy(&amp;my_int, src, sizeof(my_int); 编译为内存中的 4 字节加载指令。 (或者,如果已知 src 是对齐的,它也会为 MIPS 或旧 ARM 等 ISA 执行此操作。)您通常不会调用库函数,或者复制到堆栈内存并从那里重新加载.
  • 没错,编译器在这方面做得更好。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-11-23
  • 1970-01-01
  • 1970-01-01
  • 2014-11-12
  • 1970-01-01
  • 2016-03-20
相关资源
最近更新 更多