Почему цикл поиска целочисленных массивов в C ++ медленнее, чем в Java? - PullRequest
26 голосов
/ 19 мая 2019

У меня одна и та же программа, написанная на C ++ и Java.Для C ++ я использую VS 2019, а для Java - Eclipse 2019-03.

Вот программа на C ++.

#define InputSize 500000

int FindDuplicate::FindDuplicateNaive(int* input, int size)
{
    int j;
    for (int i = 0; i < size-1; i++)
    {
        for ( j= i+1; j < size; j++)
        {
            if (input[i] == input[j])
                return input[i];
        }
    }
    return -1;
}

int* FindDuplicate::CreateTestCase(int size)
{
    int* output = new int[size];
    int i;
    for ( i= 0; i < size-1; i++)
    {
        output[i] = i + 1;
    }
    output[i] = i;
    return output;
}

int main()
{

    int* input= FindDuplicate::CreateTestCase(InputSize);
    auto start = std::chrono::system_clock::now();//clock start 
    int output = FindDuplicate::FindDuplicateNaive(input, InputSize);
    auto end = std::chrono::system_clock::now();//clock end
    cout<<"Output is: "<<output<<endl;
    std::chrono::duration<double> elapsed_seconds = end - start;
    cout<< "elapsed time: " << elapsed_seconds.count() << "s\n";

}

Вот программа Java ...

public class FindDuplicate {

public static int FindDuplicateNaive(int[] input) {
    for (int i = 0; i < input.length - 1; i++) {
        for (int j = i + 1; j < input.length; j++) {
            if (input[i] == input[j])
                return input[i];
        }
    }
    return -1;
}

    public static int[] CreateTestCase(int n) {
    // 1, 2, 3, 4, 5, 1 = n = 6
    int[] output = new int[n];
    int i;
    for (i = 0; i < n - 1; i++) {
        output[i] = i + 1;
    }
    output[i] = i;
    return output;
}
    public static void main(String[] args) 
{
    //Here also args[0] is 5,00,000
    int number = Integer.parseInt(args[0]);

    int[] input = CreateTestCase(number);

    long start = System.currentTimeMillis();

    int output = FindDuplicateNaive(input);

    long end = System.currentTimeMillis();

    System.out.println("Total time taken is: " + (end - start) / 1000.0 + " secs");

    System.out.println(output);
}

Вы будете шокированы, узнав, сколько времени у одной и той же программы занято одним и тем же вводом в c ++ и Java.

В Java:

Время, затраченное на выполнение: 41,876 с
499999

В CPP:

После включения оптимизации и в режиме деблокирования,

Выход: 499999
истекшее время: 64,0293 с

Любая мысль об этом, в чем может быть причина?Почему Java длится 41,876 секунды, а CPP - 64,0293 секунды ??

Ответы [ 3 ]

16 голосов
/ 20 мая 2019

Поскольку векторизация не может быть легко осуществлена, большую часть времени проводят в циклическом управлении.
Благодаря использованию #pragma GCC unroll N во внутреннем цикле, который помогает исследовать, развертывание цикла обеспечивает объяснение результатов ОП.

Я получаю эти средние результаты (консоль исключена из таймингов):

gcc 8.3, -03, unroll 64    1.63s
gcc 8.3, -03, unroll 32    1.66s
gcc 8.3, -03, unroll 16    1.71s
gcc 8.3, -03, unroll 8     1.81s
gcc 8.3, -03, unroll 4     1.97s
gcc 8.3, -03, unroll 2     2.33s
gcc 8.3, -03, no unroll    3.06s
openjdk 10.0.2             1.93s

edit: эти тесты были запущены с InputSize = 100'000, как в исходном вопросе (впоследствии измененном на 500'000)

13 голосов
/ 20 мая 2019

Основное различие заключается в развертывании цикла.

Java развернул внутренний цикл очень хитро, в то время как GCC / clang / MSVC / ICC не развернул его (это пропущенная оптимизация этих компиляторов).

Если вы вручную развернете цикл, вы можете ускорить его, чтобы получить скорость, аналогичную java-версии, примерно так:

for ( j= i+1; j < size-3; j+=4)
{
    if (input[i] == input[j])
        return input[i];
    if (input[i] == input[j+1])
        return input[i];
    if (input[i] == input[j+2])
        return input[i];
    if (input[i] == input[j+3])
        return input[i];
}
for (; j < size; j++)
{
    if (input[i] == input[j])
        return input[i];
}

Для доказательства, вот внутренний цикл java-версии(8 раз развернуть):

  0x00007f13a5113f60: mov     0x10(%rsi,%rdx,4),%ebx  ;*iaload
                                                ; - FindDuplicate::FindDuplicateNaive@25 (line 6)

  0x00007f13a5113f64: cmp     %ebx,%ecx
  0x00007f13a5113f66: je      0x7f13a5113fcb    ;*if_icmpne
                                                ; - FindDuplicate::FindDuplicateNaive@26 (line 6)

  0x00007f13a5113f68: movsxd  %edx,%rdi
  0x00007f13a5113f6b: mov     0x14(%rsi,%rdi,4),%ebx  ;*iaload
                                                ; - FindDuplicate::FindDuplicateNaive@25 (line 6)

  0x00007f13a5113f6f: cmp     %ebx,%ecx
  0x00007f13a5113f71: je      0x7f13a5113fc9    ;*if_icmpne
                                                ; - FindDuplicate::FindDuplicateNaive@26 (line 6)

  0x00007f13a5113f73: mov     0x18(%rsi,%rdi,4),%ebx  ;*iaload
                                                ; - FindDuplicate::FindDuplicateNaive@25 (line 6)

  0x00007f13a5113f77: cmp     %ebx,%ecx
  0x00007f13a5113f79: je      0x7f13a5113fed    ;*if_icmpne
                                                ; - FindDuplicate::FindDuplicateNaive@26 (line 6)

  0x00007f13a5113f7b: mov     0x1c(%rsi,%rdi,4),%ebx  ;*iaload
                                                ; - FindDuplicate::FindDuplicateNaive@25 (line 6)

  0x00007f13a5113f7f: cmp     %ebx,%ecx
  0x00007f13a5113f81: je      0x7f13a5113ff2    ;*if_icmpne
                                                ; - FindDuplicate::FindDuplicateNaive@26 (line 6)

  0x00007f13a5113f83: mov     0x20(%rsi,%rdi,4),%ebx  ;*iaload
                                                ; - FindDuplicate::FindDuplicateNaive@25 (line 6)

  0x00007f13a5113f87: cmp     %ebx,%ecx
  0x00007f13a5113f89: je      0x7f13a5113ff7    ;*if_icmpne
                                                ; - FindDuplicate::FindDuplicateNaive@26 (line 6)

  0x00007f13a5113f8b: mov     0x24(%rsi,%rdi,4),%ebx  ;*iaload
                                                ; - FindDuplicate::FindDuplicateNaive@25 (line 6)

  0x00007f13a5113f8f: cmp     %ebx,%ecx
  0x00007f13a5113f91: je      0x7f13a5113ffc    ;*if_icmpne
                                                ; - FindDuplicate::FindDuplicateNaive@26 (line 6)

  0x00007f13a5113f93: mov     0x28(%rsi,%rdi,4),%ebx  ;*iaload
                                                ; - FindDuplicate::FindDuplicateNaive@25 (line 6)

  0x00007f13a5113f97: cmp     %ebx,%ecx
  0x00007f13a5113f99: je      0x7f13a5114001    ;*if_icmpne
                                                ; - FindDuplicate::FindDuplicateNaive@26 (line 6)

  0x00007f13a5113f9b: mov     0x2c(%rsi,%rdi,4),%ebx  ;*iaload
                                                ; - FindDuplicate::FindDuplicateNaive@25 (line 6)

  0x00007f13a5113f9f: cmp     %ebx,%ecx
  0x00007f13a5113fa1: je      0x7f13a5114006    ;*if_icmpne
                                                ; - FindDuplicate::FindDuplicateNaive@26 (line 6)

  0x00007f13a5113fa3: add     $0x8,%edx         ;*iinc
                                                ; - FindDuplicate::FindDuplicateNaive@33 (line 5)

  0x00007f13a5113fa6: cmp     %r8d,%edx
  0x00007f13a5113fa9: jl      0x7f13a5113f60    ;*if_icmpge
                                                ; - FindDuplicate::FindDuplicateNaive@17 (line 5)
9 голосов
/ 19 мая 2019

Это не полный ответ, я не могу объяснить, почему на самом деле он работает быстрее в Java, чем в C ++; но я могу объяснить пару вещей, которые сдерживают производительность вашей версии C ++. Пожалуйста, не выбирайте этот ответ как правильный, если у кого-то есть реальное объяснение общей разницы в производительности.

Этот ответ обсуждался на мета и был согласован с тем, что оставить его в качестве частичного ответа временно - лучший вариант.


Во-первых, и самое главное, как уже упоминалось в комментариях, Java-код уже оптимизирован при его тестировании, тогда как в C ++ вы должны указывать уровень оптимизации в качестве аргумента командной строки (формировать визуальную студию и компилировать как выпуск), и хотя это имеет большое значение, в моих тестах Java все еще находится на вершине (все результаты внизу).

Но я хочу указать на серьезный недостаток в вашем тесте, который может показаться незначительным в данном конкретном случае, поскольку он не имеет большого значения, когда вы смотрите на цифры, но все же важен: Операции ввода-вывода добавляют заметные задержки. Для точного сравнения времени выполнения вы должны исключить операции ввода-вывода из вашего таймера на обоих языках. Хотя в этом случае это не имеет большого значения, если один язык выполняет и функцию, и вывод во время работы таймера, а другой - только функцию, что делает весь ваш тест смещенным и бессмысленным.

Чтобы сделать его более эквивалентным версии Java, измените основную часть c ++ на

int main()
    {
    int* input = FindDuplicate::CreateTestCase(InputSize);   

    int result;
    auto start = std::chrono::system_clock::now(); //clock start 
    result = FindDuplicate::FindDuplicateNaive(input, InputSize);
    auto end = std::chrono::system_clock::now(); //clock end

    std::chrono::duration<double> elapsed_seconds = end - start;
    cout << "Output is: " << result << endl;
    cout << "elapsed time: " << elapsed_seconds.count() << "s\n";
    }

Обратите внимание, что по умолчанию консольный ввод / вывод C ++ (iostream, cin / cout) даже медленнее, чем мог бы быть, потому что синхронизация с консольным вводом / выводом C (stdio, scanf / printf) включена, чтобы программа не выполняла странные вещи, если используются и cout, и printf. Здесь вы можете прочитать о производительности cout при выключенной синхронизации. Мало того, что вы использовали ввод-вывод внутри ограничений таймера, вы даже использовали его в режиме худшей производительности.

Вот мои результаты, которые, хотя и дают Java преимущество, показывают, сколько различий могут иметь определенные опции компиляции и манипуляции ввода-вывода в C ++ (для одного cout разница в среднем на 0,03 с при отключении синхронизации больше, чем это выглядит). Все значения в секундах являются средними для 10 тестов.

1. Java print in timer                   1.52s
2. Java                                  1.36s
3. C++  debug, cout in timer            11.78s
4. C++  debug                           11.73s
5. C++  release, cout in timer           3.32s
6. C++  release cout syncronization off  3.29s
7. C++  release                          3.26s

Я хочу, чтобы вы поняли, что из всех этих тестов единственные, из которых имеет смысл сравнение, это 1 с 6 и 2 с 7 . Все остальные (3, 4, 5) будут сравнивать, независимо от того, сколько раз вы повторяете тест.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...