10

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; }

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)
4

3 に答える 3

8

発行されたコードを確認する必要があります。memcpy明らかに、オプティマイザーは、潜在的にアライメントされていない単一のサイズの読み取りを戻り値に変換できる「はず」ですがint、異なる時間が表示された場合、x86 ではそうではないことを意味します。

私のマシンでは、gcc を使用し-O2て常に 0.09 を取得します。-O3I get 0 for all times (それが時間の粒度よりも速いかどうか、またはオプティマイザーがすべてのコードを削除したかどうかは確認していません) 。

おそらく、正しいコンパイラ フラグを使用していない (または ideone を使用していない) というのが答えです。

アライメントされていない可能性のある読み取りがアライメントされた読み取りとは異なる命令を必要とするアーキテクチャでは、 はアライメントされていない読み取りを発行する必要があるreinterpret_cast一方で、memcpyはアライメントされていない読み取りを発行する必要があります (関数の呼び出し方法によって異なります。この場合、データは実際には整列されていますが、コンパイラーがそれを証明できる条件はわかりません)。その場合、reinterpret_castコードは よりも高速になると予想memcpyされますが、アラインされていないポインターが渡された場合は、もちろん正しくありません。

于 2012-10-29T09:41:17.537 に答える
6

キャストはコンパイル時の操作memcpy()ですが、実行時の操作です。これが、キャストが実行時間に影響を与えない理由です。

于 2012-10-29T07:06:10.627 に答える
3

memcpyレジスタにコピーすることはできません。メモリからメモリへのコピーを行います。reinterpret_castinは、レジスタに保持されているポインタの型を変更できます。これget_int_v1には、レジスタ間のコピーも必要ありません。

于 2012-10-29T07:33:36.010 に答える