Java превосходит C++ в сравнении скорости рекурсивного алгоритма в моем простом тесте

Благодаря этому алгоритму «разделяй и властвуй» (Programming Pearls p80) для нахождения максимальной суммы, найденной в любом непрерывном подвекторе массива, программа на Java работает быстрее, чем аналог C++, протестированный на Win7 x64 с 8 ГБ ОЗУ.

И Java, и C++ работают на 1 ядре ЦП.

Какая оптимизация делается на JVM, чтобы сделать это возможным?

JVM 1 используется:

Версия Java "1.6.0_21"
Java(TM) SE Runtime Environment (сборка 1.6.0_21-b07)
Java HotSpot(TM) 64-разрядная виртуальная машина сервера (сборка 17.0-b17, смешанный режим)
Аргумент виртуальной машины - Xmx12000m

Используется JVM 2: jrockit-jdk1.6.0_24-R28.1.3-4.0.1
Аргумент VM -Xmx12000m

Компилятор С++:

Компилятор Microsoft по умолчанию, который поставляется с Visual Studio 2008 x64.

Время:

 //Java JVM 1, Oracle JRE
 //0x1fffffff: about 38s, sum 144115187270549505
 //0x2fffffff: about 56s, sum 324259171962716161
 //0x3fffffff: about 81s, sum 576460750692810753

 //Java JVM 2, Oracle JRockit jrockit-jdk1.6.0_24-R28.1.3-4.0.1
 //0x1fffffff: about 46s
 //0x2fffffff: about 69s
 //0x3fffffff: about 95s     

 //Cpp
 //0x1fffffff: around 45s, x64 Release
 //0x2fffffff: around 68s, x64 Release sum: 324259171962716161
 //0x3fffffff: around 93s, x64 Release sum: 576460750692810753, 
final int MAX = 0x3fffffff; 
Pearls1 pearls1 = new Pearls1();
pearls1.arr = new int[MAX];
for (int i = 0; i < MAX; i++)
{
    pearls1.arr[(int) i] = i;                
}
long startTime = System.nanoTime();
long sum = pearls1.binaryForce(0, MAX - 1);
long endTime = System.nanoTime();

long binaryForce(long lower, long upper) {
    //std::cout << "binaryForce("<< lower << ","<< upper <<")" << std::endl;

    if( lower > upper ) {
        return 0;
    } 
    if( lower == upper ) {      
        return Math.max( 0L, arr[(int) lower] ) ;       
    }

    long middle = ( lower + upper ) /2 ;

    long lmax = 0,  sum = 0;

    for( long i = middle; i >=lower; i-- ) {
        sum += arr[(int) i];        
        lmax = Math.max( lmax, sum);
    }

    long rmax = 0;
    sum = 0;

    //for( long i = middle+1; i <= upper; i++ ) {
    for( long i = upper; i > middle; i-- ) {
        sum += arr[(int) i];
        rmax = Math.max(rmax, sum);     
    }

    long theMax = lmax+rmax;

    long binarySumLeft = binaryForce(lower, middle);
    long binarySumRight = binaryForce(middle+1, upper);

    if( theMax > binarySumLeft && theMax > binarySumRight ) {
        return theMax;
    }
    else if( binarySumLeft > theMax && binarySumLeft > binarySumRight ) {
        return binarySumLeft;
    }
    else if ( binarySumRight > theMax && binarySumRight > binarySumLeft ) {
        return binarySumRight;
    }

    else {
        return theMax;      
    }

}
 int main(...) {
    MAX = 0x3fffffff;
    arr = new long[MAX];
    for( long i=0;i<MAX;i++) {
        //arr[i] = rand();
        arr[i] = i;
    }

    timeb startTime, endTime;
    ftime( &startTime);
    std::cout << "Start time: " << startTime.time << " sec, " << startTime.millitm << " ms" << std::endl;

    sum = binaryForce(0, MAX-1);
    std::cout << "sum: " << sum <<std::endl;
    ftime( &endTime);
    std::cout << "End time: " << endTime.time << " sec, " << endTime.millitm << " ms" << std::endl;

    long runTimeSec = endTime.time - startTime.time;
    long runTimeMs = endTime.millitm - startTime.millitm;

    std::cout << "Run time: " << runTimeSec << " sec, " << runTimeMs << " ms" << std::endl;
 }

long long binaryForce(long lower, long upper) {
    //std::cout << "binaryForce("<< lower << ","<< upper <<")" << std::endl;

    if( lower > upper ) {
        return 0;
    } 
    if( lower == upper ) {        
        return std::max( 0L, arr[lower] ) ;        
    }

    long middle = ( lower + upper ) /2 ;

    long long lmax = 0,  sum = 0;

    for( long i = middle; i >=lower; i-- ) {
        sum += arr[i];        
        lmax = std::max( lmax, sum);
    }

    long long rmax = 0;
    sum = 0;

    //for( long i = middle+1; i <= upper; i++ ) {
    for( long i = upper; i > middle; i-- ) {
        sum += arr[i];
        rmax = std::max(rmax, sum);        
    }

    long long theMax = lmax+rmax;

    long long binarySumLeft = binaryForce(lower, middle);
    long long binarySumRight = binaryForce(middle+1, upper);

    if( theMax > binarySumLeft && theMax > binarySumRight ) {
        //std::cout << arr[theMax] << std::endl;
        return theMax;
    }
    else if( binarySumLeft > theMax && binarySumLeft > binarySumRight ) {
        //std::cout << arr[binarySumLeft] << std::endl;
        return binarySumLeft;
    }
    else if ( binarySumRight > theMax && binarySumRight > binarySumLeft ) {
        //std::cout << arr[binarySumRight] << std::endl;
        return binarySumRight;
    }

    else {
        //std::cout << arr[theMax] << std::endl;
        return theMax;        
    }
}

person Lydon Ch    schedule 26.04.2011    source источник
comment
Где код С++?   -  person    schedule 26.04.2011
comment
Это также зависит от вашего кода С++... вы также должны его предоставить.   -  person Vincent Mimoun-Prat    schedule 26.04.2011
comment
Оба процесса работают с одинаковой битовой глубиной? (32 или 64)   -  person helios    schedule 26.04.2011
comment
Вы включаете оптимизацию для компилятора C++ (например, -O3 с gcc)   -  person Simon Nickerson    schedule 26.04.2011
comment
@FrustratedWithFormsDesigner Он делает это, посмотрите комментарий в разделе кода   -  person Thomas    schedule 26.04.2011
comment
@portoalet: я голосую за закрытие. Вы не предоставляете используемые тесты или реальные показатели производительности (кроме всех 6 совокупных чисел), используемый код C++ или компилятор/среду выполнения, используемые для каждого языка.   -  person phooji    schedule 26.04.2011
comment
Нет соответствующего кода C++ = не настоящий вопрос.   -  person James McNellis    schedule 26.04.2011
comment
Моя синяя машина побеждает красные машины на скорости по прямой, если только она не была оптимизирована с помощью гоночной полосы.   -  person Michael    schedule 26.04.2011
comment
Вы используете long long в версии C++ и long в версии java. 128-битная арифметика определенно будет медленнее.   -  person Emile Cormier    schedule 26.04.2011
comment
хм, long long в визуальной студии - это 64 бита, верно? Когда я использую только длинный, он усекается до отметки 4 миллиарда.   -  person Lydon Ch    schedule 26.04.2011
comment
@phooji @James McNellis: добавлен код cpp и используемый jvm, а также типы компиляторов, снова открыть?   -  person Lydon Ch    schedule 26.04.2011
comment
@portoalet: Ой, я думаю, ты прав.   -  person Emile Cormier    schedule 26.04.2011
comment
Ничего не стоит тот факт, что Java особенно хорошо обнаруживает код, который ничего не делает, и устраняет его. (Возможно, лучше, чем C++, который имеет статические средства, такие как макросы для отключения кода.) Эта функция может быть не так полезна в реальных программах. Я бы попробовал последнюю версию Java 6, так как в ней есть ряд улучшений JVM.   -  person Peter Lawrey    schedule 26.04.2011
comment
Какие флаги вы передаете cl при компиляции программы на C++? /O2? /DEBUG?   -  person James McNellis    schedule 26.04.2011
comment
это релиз, поэтому /NDEBUG и да /O2 для максимальной скорости   -  person Lydon Ch    schedule 26.04.2011


Ответы (2)


Java использует компилятор Just-in-Time во время выполнения для компиляции байт-кода в соответствующий машинный код для архитектуры, на которой вы работаете. Во время работы он собирает метрики выполнения, чтобы проверить, что делает код; если он определяет более оптимальный механизм, который не изменяет результаты кода, он перекомпилирует работающий код, что означает, что он оптимизирован для наиболее часто используемых путей.

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

Вы также не говорите, какую JVM вы используете. Различные JVM имеют разные характеристики. JRockit, например, на самом деле оптимизируется намного лучше, чем стандартная JVM Oracle, хотя для прогрева требуется гораздо больше времени.

person Joseph Ottinger    schedule 26.04.2011
comment
В принципе, интересно проанализировать, что именно делает JVM, чтобы приведенный выше код работал быстрее, чем при статическом анализе; тем более, что разгон довольно большой. Тем не менее, вопросы, которые вы поднимаете в своем последнем абзаце, действительны. - person Konrad Rudolph; 26.04.2011
comment
Конечно, это определенно интересный вопрос, но рассмотрим простой путь кода: if(a()) {b();} else {c();}. Что, если a() изменяется очень редко, но обычно возвращает false, а блок else занимает больше времени, чем блок после if()? Статическая оптимизация не может определить, какое значение a() даст чаще всего; он должен использовать код, как указано. JVM может инвертировать код по своему усмотрению, чтобы дать приоритет выполнения вызову c(). А если в какой-то момент a() возвращает true большую часть времени? JVM может снова перекомпилировать блок. - person Joseph Ottinger; 26.04.2011
comment
@ Джозеф Я знаю об этом, и да, это может случиться. Я хочу сказать: было бы действительно интересно исследовать реальный случай такого примера (например, в проблеме ОП) и посмотреть, как такая оптимизация действительно работает. Во-первых, сбор метрик выполнения должен иметь значительные накладные расходы. Когда JVM останавливается? Точно так же, как это повлияет на профильную оптимизацию эквивалентного кода C++? Интересные вопросы, которые стоит исследовать (но с более четко определенной проблемой). Тем не менее, ваш ответ получил мой +1. - person Konrad Rudolph; 26.04.2011
comment
Спасибо - и JVM не останавливается. Для некоторых JVM вы можете передать аргументы JVm, чтобы увидеть, какие оптимизации он делает (за счет скорости выполнения, поскольку это диагностическая информация) - действительно поучительно видеть, когда и какие оптимизации он делает. И в реальном мире это, очевидно, важная функция, хотя она также делает недействительными все глупые C++ быстрее, чем Java, а Java быстрее, чем аргументы C++, IMO, поскольку это действительно зависит от реальных проблем, над которыми работают. - person Joseph Ottinger; 26.04.2011
comment
@Joseph Видите ли, как человек, который ничего не знает об оптимизации JIT, вы меня зацепили: как JVM может продолжать инструментировать код, не неся накладных расходов, которые сведут на нет какой-либо прирост производительности? Я должен расследовать это… - person Konrad Rudolph; 26.04.2011
comment
@portoalet: будьте готовы ждать — разогрев JRockit может длиться час. После завершения прогрева, в зависимости от характера измеряемого кода, улучшения могут быть радикальными и довольно неожиданными. @Konrad: конечно, ЕСТЬ накладные расходы. Но накладные расходы очень незначительны, и прибыль того стоит. Люди, разрабатывающие эти JVM, просто пугают своей чертовски сообразительностью. - person Joseph Ottinger; 26.04.2011
comment
Протестировано быстро с готовым jrockit-jdk1.6.0_24-R28.1.3-4.0.1, он был медленнее, чем обычный JRE1.6 примерно на 15% - person Lydon Ch; 26.04.2011
comment
@Джозеф Оттингер, я запускаю его как консольное приложение, а не как серверное, поэтому я не понимаю, когда вы говорите, что разминка JRockit может длиться час. - person Lydon Ch; 26.04.2011
comment
@portoalet: совсем не удивительно. Нет никакой разницы между консольным приложением и серверным приложением — оба они просто приложения, работающие в JVM. Совсем не удивительно, что JRockit был медленнее в тесте, который длился менее двух минут. Если тест длится более часа, тогда я бы начал задаваться вопросом, что происходит, если бы вы не увидели, что JRockit немного ускорился. - person Joseph Ottinger; 26.04.2011
comment
@Джозеф Оттингер: пример с оператором if требует дополнительных нюансов. Да, в принципе, JVM может перекомпилироваться на основе частоты ветвлений. Однако для одного оператора if с сильным уклоном в сторону одной из ветвей аппаратный предсказатель ветвления будет намного эффективнее, чем перекомпиляция кода на основе пути, поскольку он практически не требует дополнительных затрат. - person phooji; 26.04.2011
comment
@phooji: Согласен. Мой пример должен был проиллюстрировать концепцию, а не действительность, поскольку на SO мы ограничены ~ 500 символами в комментариях - нюансы трудно передать. Это не должно было быть детерминированным утверждением. - person Joseph Ottinger; 26.04.2011
comment
Просто немного странно, почему в моем тесте JRockit работает на 15% медленнее, чем обычная JRE. Должно быть, это было оптимизировано каким-то другим способом, как сказал @Joseph Ottinger. - person Lydon Ch; 26.04.2011
comment
Нет, при запуске JRockit почти не оптимизируется. Так что логично, что вы получаете низкую производительность из-за кода, который не предназначен для хорошей работы. - person Joseph Ottinger; 26.04.2011

Я предполагаю, что int в С++ представлен как 32 бита (4 байта) - в 64-битной системе ядру необходимо выполнить 2 операции для доступа к int: 1/прочитать СЛОВО (64 бита), затем 2/применить маска И, чтобы уменьшить это до 32 бит. Насколько я знаю, JVM, вероятно, будет хранить внутри int как 64-битный, но только в тот момент, когда вы читаете это, вы получите побитовый оператор AND (т. крышка). Попробуйте изменить код C++, чтобы использовать структуру данных WORD (64 бита) или запустить то же самое на 32-битном процессоре.

person Liv    schedule 26.04.2011
comment
Нет необходимости применять маску И. - person Konrad Rudolph; 26.04.2011
comment
Аналогично: Как вы думаете, зачем нужна операция? Можете ли вы вспомнить какой-нибудь пример, где это так? Маска не «уменьшает… до 32 бит». В зависимости от того, где находится целое число (регистр, кеш), действительно может потребоваться сокращение (или нет!), но это делается простым копированием четырех байтов вместо восьми. - person Konrad Rudolph; 26.04.2011
comment
извините, я вижу, где происходит путаница - это не уменьшит его до 32 бит, но сотрет первые 32 бита (в случае переполнения). Это то, что я имел в виду - надеюсь, теперь это имеет смысл? - person Liv; 26.04.2011
comment
Стирание этих битов обычно не требуется. Переполнение, которое может произойти, может (и будет) просто игнорироваться. Есть операторы, которые не могут игнорировать переполнение, но таких операторов нет в C++. - person Konrad Rudolph; 26.04.2011