Pertanyaan mengapa c ++ std :: max_element sangat lambat?


Saya perlu mencari elemen maks dalam vektor jadi saya menggunakan std::max_element, tetapi saya telah menemukan bahwa itu adalah fungsi yang sangat lambat, jadi saya menulis versi saya sendiri dan berhasil mendapatkan kinerja yang lebih baik x3, berikut adalah kodenya:

#include <string>
#include <iostream>
#include <vector>
#include <algorithm>

#include <sys/time.h>

double getRealTime()
{
    struct timeval tv;
    gettimeofday(&tv, 0);
    return (double) tv.tv_sec + 1.0e-6 * (double) tv.tv_usec;
}

inline int my_max_element(const std::vector<int> &vec, int size)
{
    auto it = vec.begin();
    int max = *it++;
    for (; it != vec.end(); it++)
    {
        if (*it > max)
        {
            max = *it;
        }
    }
    return max;
}

int main()
{
    const int size = 1 << 20;
    std::vector<int> vec;
    for (int i = 0; i < size; i++)
    {
        if (i == 59)
        {
            vec.push_back(1000000012);
        }
        else
        {
            vec.push_back(i);
        }
    }

    double startTime = getRealTime();
    int maxIter = *std::max_element(vec.begin(), vec.end());
    double stopTime = getRealTime();
    double totalIteratorTime = stopTime - startTime;

    startTime = getRealTime();
    int maxArray = my_max_element(vec, size);
    stopTime = getRealTime();
    double totalArrayTime = stopTime - startTime;

    std::cout << "MaxIter = " << maxIter << std::endl;
    std::cout << "MaxArray = " << maxArray << std::endl;
    std::cout << "Total CPU time iterator = " << totalIteratorTime << std::endl;
    std::cout << "Total CPU time array = " << totalArrayTime << std::endl;
    std::cout << "iter/array ratio: = " << totalIteratorTime / totalArrayTime << std::endl;
    return 0;
}

Keluaran:

MaxIter = 1000000012
MaxArray = 1000000012
Total CPU time iterator = 0.000989199
Total CPU time array = 0.000293016
iter/array ratio: = 3.37592

rata-rata std::max_element membutuhkan waktu x3 lebih lama my_max_element. Jadi mengapa saya dapat membuat fungsi std lebih cepat dengan mudah? Haruskah saya berhenti menggunakan std dan menulis fungsi saya sendiri karena std sangat lambat?

Catatan: pada awalnya saya meskipun itu karena saya menggunakan dan integer i dalam lingkaran bukan iterator, tetapi pelipit itu tidak menjadi masalah sekarang.

Info kompilasi:

g ++ (GCC) 4.8.2

g ++ -O3 -Wall -c -fmessage-length = 0 -std = c + + 0x


32
2017-09-02 11:16


asal


Jawaban:


Sebelum memberikan suara pada jawaban ini, silakan uji (dan verifikasi) ini di mesin Anda dan komentar / tambahkan hasilnya. Perhatikan bahwa saya menggunakan ukuran vektor 1000 * 1000 * 1000 untuk pengujian saya. Saat ini, jawaban ini memiliki 19 upvotes tetapi hanya satu hasil yang diposting, dan hasil ini tidak menunjukkan efek yang dijelaskan di bawah ini (meskipun diperoleh dengan kode pengujian yang berbeda, lihat komentar).


Sepertinya ada bug / artefak pengoptimal. Bandingkan waktu:

template<typename _ForwardIterator, typename _Compare>
_ForwardIterator
my_max_element_orig(_ForwardIterator __first, _ForwardIterator __last,
_Compare __comp)
{
  if (__first == __last) return __first;
  _ForwardIterator __result = __first;

  while(++__first != __last)
    if (__comp(__result, __first))
      __result = __first;

  return __result;
}

template<typename _ForwardIterator, typename _Compare>
_ForwardIterator
my_max_element_changed(_ForwardIterator __first, _ForwardIterator __last,
_Compare __comp)
{
  if (__first == __last) return __first;
  _ForwardIterator __result = __first;
  ++__first;

  for(; __first != __last; ++__first)
    if (__comp(__result, __first))
      __result = __first;

  return __result;
}

Yang pertama adalah implementasi asli libstdc ++, yang kedua harus berupa transformasi tanpa perubahan dalam perilaku atau persyaratan. Clang ++ menghasilkan waktu berjalan yang sangat mirip untuk kedua fungsi tersebut, sedangkan g ++ 4.8.2 empat kali lebih cepat dengan versi kedua.


Mengikuti proposal Maxim, mengubah vektor dari int untuk int64_t, versi yang diubah tidak 4, tetapi hanya 1,7 kali lebih cepat dari versi asli (g + + 4.8.2).


Perbedaannya adalah dalam meramalkan prediktif *result, yaitu, menyimpan nilai elemen maks saat ini sehingga tidak perlu dimuat ulang dari memori setiap kali. Ini memberikan pola akses cache yang jauh lebih bersih:

w/o commoning     with commoning
*                 *
**                 *
 **                 *
  **                 *
  * *                 *
  *  *                 *
  *   *                 *

Inilah asm untuk perbandingan (rdi/rsi berisi iterasi pertama / terakhir):

Dengan loop while (2.88743 ms; inti):

    movq    %rdi, %rax
    jmp .L49
.L51:
    movl    (%rdi), %edx
    cmpl    %edx, (%rax)
    cmovl   %rdi, %rax
.L49:
    addq    $4, %rdi
    cmpq    %rsi, %rdi
    jne .L51

Dengan for loop (1235.55 μs):

    leaq    4(%rdi), %rdx
    movq    %rdi, %rax
    cmpq    %rsi, %rdx
    je  .L53
    movl    (%rdi), %ecx
.L54:
    movl    (%rdx), %r8d
    cmpl    %r8d, %ecx
    cmovl   %rdx, %rax
    cmovl   %r8d, %ecx
    addq    $4, %rdx
    cmpq    %rdx, %rsi
    jne .L54
.L53:

Jika saya memaksa umum dengan menyimpan secara eksplisit *result menjadi variabel prev di awal dan kapan saja result diperbarui, dan menggunakan prev dari pada *result dalam perbandingan, saya mendapatkan loop yang lebih cepat (377.601 μs):

    movl    (%rdi), %ecx
    movq    %rdi, %rax
.L57:
    addq    $4, %rdi
    cmpq    %rsi, %rdi
    je  .L60
.L59:
    movl    (%rdi), %edx
    cmpl    %edx, %ecx
    jge .L57
    movq    %rdi, %rax
    addq    $4, %rdi
    movl    %edx, %ecx
    cmpq    %rsi, %rdi
    jne .L59
.L60:

Alasannya ini lebih cepat daripada for lingkaran adalah bahwa bergerak bersyarat (cmovl) di atas adalah pesimisme karena dieksekusi sangat jarang (Kata Linus bahwa cmov hanya ide yang baik jika cabang tidak dapat diprediksi). Perhatikan bahwa untuk data yang didistribusikan secara acak, cabang diharapkan akan diambil Hn kali, yang merupakan proporsi yang dapat diabaikan (Hn tumbuh secara logaritmik, jadi Hn/ n dengan cepat mendekati 0). Kode bergerak bersyarat hanya akan lebih baik pada data patologis, mis. [1, 0, 3, 2, 5, 4, ...].


27
2017-09-02 11:50



Anda mungkin menjalankan tes Anda dalam mode 64-bit, di mana sizeof(int) == 4, tapi sizeof(std::vector<>::iterator) == 8, sehingga tugas dalam pengulangan ke int(apa my_max_element tidak) lebih cepat daripada std::vector<>::iterator (ini adalah apa std::max_element tidak).

Jika kamu berubah std::vector<int> untuk std::vector<long> hasil berubah menguntungkan std::max_element:

MaxIter = 1000000012
MaxArray = 1000000012
Total CPU time iterator = 0.00429082
Total CPU time array = 0.00572205
iter/array ratio: = 0.749875

Satu catatan penting: ketika benchmarking menonaktifkan skala frekuensi CPU, sehingga CPU tidak berpindah persneling di tengah benchmark.


Tapi saya pikir ada hal lain yang bermain di sini, karena hanya mengubah variabel loop dari int untuk long tidak mengubah hasil ...


9
2017-09-02 11:50



Ini masalah sederhana cache. Untuk pertama kalinya, Anda pertama kali memuat memori, dalam hal ini isi vektor, itu selalu jauh lebih lambat daripada jika baru-baru ini diakses. Saya menyalin dan menempelkan kode Anda dengan GCC 4.9.

Ketika fungsi dibalik, rasionya adalah 1. Ketika mereka berada di urutan awal, rasionya adalah 1,6.

Ini masih tampak seperti kesalahan pengoptimalan yang mendasar oleh GCC dalam kasus max_element kepada saya. Namun, waktu fungsi Anda sangat rendah, mereka akan didominasi oleh suara CPU seperti efek cache di atas, daripada perbandingan yang berarti.

Terbalik, Asli


2