Почему мой код на С++ в три раза медленнее, чем эквивалент C на LeetCode?

Я выполнял некоторые проблемы LeetCode, и я замечаю, что решения C в два раза быстрее, чем те же самые вещь в С++. Например:

Обновлено с помощью нескольких простых примеров:

Учитывая отсортированный массив и целевое значение, верните индекс, если цель найдена. Если нет, верните индекс, где он был бы, если бы он был вставлен по порядку. Вы не можете принимать дубликаты в массиве. (Ссылка на вопрос о LeetCode)

Мое решение в C работает в 3 ms:

int searchInsert(int A[], int n, int target) {
    int left = 0;
    int right = n;
    int mid = 0;
    while (left<right) {
        mid = (left + right) / 2;
        if (A[mid]<target) {
            left = mid + 1;
        }
        else if (A[mid]>target) {
            right = mid;
        }
        else {
            return mid;
        }
    }
    return left;
}

Мое другое решение на С++, точно такое же, но как функция-член класса Solution, запускается в 13 ms:

class Solution {
public:
    int searchInsert(int A[], int n, int target) {
        int left = 0;
        int right = n;
        int mid = 0;
        while (left<right) {
            mid = (left + right) / 2;
            if (A[mid]<target) {
                left = mid + 1;
            }
            else if (A[mid]>target) {
                right = mid;
            }
            else {
                return mid;
            }
        }
        return left;
    }
};

Даже более простой пример:

Обозначьте цифры целого числа. Возвращает 0, если результат будет переполняться. (Ссылка на вопрос о LeetCode)

Версия C работает в формате 6 ms:

int reverse(int x) {
    long rev = x % 10;
    x /= 10;
    while (x != 0) {
        rev *= 10L;
        rev += x % 10;
        x /= 10;
        if (rev>(-1U >> 1) || rev < (1 << 31)) {
            return 0;
        }
    }
    return rev;
}

И версия С++ точно такая же, как и функция-член класса Solution, и работает для 19 ms:

class Solution {
public:
    int reverse(int x) {
        long rev = x % 10;
        x /= 10;
        while (x != 0) {
            rev *= 10L;
            rev += x % 10;
            x /= 10;
            if (rev>(-1U >> 1) || rev < (1 << 31)) {
                return 0;
            }
        }
        return rev;
    }
};

Я вижу, как будут значительные накладные расходы от использования вектора вектора в качестве 2D-массива в исходном примере, если система тестирования LeetCode не компилирует код с включенной оптимизацией. Но более простые примеры выше не должны страдать от этой проблемы, потому что структуры данных довольно сырые, особенно во втором случае, когда все, что у вас есть, - это длинная или целая арифметика. Это еще медленнее в три раза.

Я начинаю думать, что может произойти что-то странное с тем, как LeetCode делает бенчмаркинг вообще, потому что даже в C-версии целочисленной проблемы с обращением вы получаете огромный бамп в процессе работы от просто замены строки   if (rev > (- 1U → 1) || rev < (1 < 31)) { с   if (rev > INT_MAX || rev < INT_MIN) {

Теперь я полагаю, что иметь значение #include<limits.h> может иметь какое-то отношение к этому, но кажется немного экстремальным, что это простое изменение приводит к сокращению времени выполнения всего от 6 ms до 19 ms.

Ответ 1

В последнее время я много видел предложение vector<vector<int>> для создания 2d-массивов на С++, и я указывал людям, почему это действительно не очень хорошая идея. Это удобный трюк, чтобы узнать, когда шлепает временный код, но там (почти) никогда не было причин когда-либо использовать его для реального кода. правильная вещь - использовать класс, который обертывает непрерывный блок памяти.

Итак, моя первая реакция могла бы указывать на это как на возможный источник несоответствия. Однако вы также используете int** в версии C, что обычно является признаком той же самой проблемы, что и vector<vector<int>>.

Поэтому вместо этого я решил просто сравнить два решения.

http://coliru.stacked-crooked.com/a/fa8441cc5baa0391

6468424
6588511

Это время, затраченное версией "C" на "С++-версию" в наносекундах.

Мои результаты не показывают ничего похожего на несоответствие, которое вы описываете. Затем мне пришло в голову проверить общую ошибку, которую люди совершают при бенчмаркинге

http://coliru.stacked-crooked.com/a/e57d791876b9252b

18386695
42400612

Обратите внимание, что флаг -O3 из первого примера стал -O0, что отключает оптимизацию.

Заключение: вы, вероятно, сравниваете неоптимизированные исполняемые файлы.

С++ поддерживает создание богатых абстракций, которые не требуют накладных расходов, но устранение накладных расходов требует определенных преобразований кода, которые несут хаос с "отлаживаемостью" кода.

Это означает, что отладочные сборки избегают этих преобразований, поэтому сборки отладки С++ часто медленнее, чем отладочные сборки кода стиля C, потому что код стиля C просто не использует много абстракции. Наблюдение 130-процентного замедления, такого как приведенное выше, вовсе не удивительно, когда время, например, машинный код, который использует вызовы функций вместо простых инструкций магазина.


Некоторый код действительно нуждается в оптимизации, чтобы иметь разумную производительность даже для отладки, поэтому компиляторы часто предлагают режим, который применяет некоторые оптимизации, которые не вызывают слишком больших проблем для отладчиков. Clang и gcc используют -O1 для этого, и вы можете видеть, что даже этот уровень оптимизации существенно устраняет пробел в этой программе между кодом стиля C и кодом стиля С++:

http://coliru.stacked-crooked.com/a/13967ebcfcfa4073

8389992
8196935


Update:

В этих более поздних примерах оптимизация не должна иметь значения, поскольку С++ не использует абстракции, кроме того, что делает версия C. Я предполагаю, что объяснение этого заключается в том, что примеры компилируются с разными компиляторами или с некоторыми другими вариантами компилятора. Не зная, как выполняется компиляция, я бы сказал, что нет смысла сравнивать эти числа во время выполнения; LeetCode явно не производит сравнение яблок с яблоками.

Ответ 2

Вы используете вектор вектора в своем фрагменте кода на С++. Векторы представляют собой контейнеры последовательностей в С++, которые похожи на массивы, которые могут меняться по размеру. Вместо vector<vector<int>>, если вы используете статически распределенные массивы, это будет лучше. Вы можете использовать свой собственный класс Array, а также с перегрузкой оператора [], но вектор имеет больше накладных расходов, поскольку он динамически изменяет размер, когда вы добавляете больше элементов, чем его первоначальный размер. В С++ вы используете вызов по ссылке, чтобы еще больше сократить время, если вы сравните это с C. С++ должен работать еще быстрее, если он хорошо написан.