Java масштабируется намного хуже, чем C #, по многим ядрам?

Я тестирую создание множества потоков, выполняющих одну и ту же функцию на 32-ядерном сервере для Java и C #. Я запускаю приложение с 1000 итерациями функции, которая распределяется по 1,2,4,8, 16 или 32 потокам с использованием пула потоков.

При 1, 2, 4, 8 и 16 параллельных потоках Java как минимум в два раза быстрее, чем C #. Однако по мере увеличения количества потоков разрыв сокращается, и на 32 потока C # имеет почти такое же среднее время выполнения, но Java иногда занимает 2000 мс (тогда как оба языка обычно работают около 400 мс). Java начинает ухудшаться из-за огромных всплесков времени, затрачиваемого на итерацию потока.

ИЗМЕНИТЬ Это Windows Server 2008

EDIT2 Я изменил приведенный ниже код, чтобы показать использование пула потоков службы Executor. Я также установил Java 7.

Я установил следующие оптимизации в виртуальной машине точки доступа:

-XX: + UseConcMarkSweepGC -Xmx 6000

но это все равно не улучшило положение вещей. Единственная разница между кодом заключается в том, что я использую пул потоков ниже и для версии C #, которую мы используем:

http://www.codeproject.com/Articles/7933/Smart-Thread-Pool

Есть ли способ сделать Java более оптимизированной? Возможно, вы могли бы объяснить, почему я наблюдаю такое резкое снижение производительности?

Есть ли более эффективный пул потоков Java?

(Обратите внимание, я не имею в виду изменение функции тестирования)

import java.io.DataOutputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

public class PoolDemo {

    static long FastestMemory = 2000000;
    static long SlowestMemory = 0;
    static long TotalTime;
    static int[] FileArray;
    static DataOutputStream outs;
    static FileOutputStream fout;
    static Byte myByte = 0;

  public static void main(String[] args) throws InterruptedException, FileNotFoundException {

        int Iterations = Integer.parseInt(args[0]);
        int ThreadSize = Integer.parseInt(args[1]);

        FileArray = new int[Iterations];
        fout = new FileOutputStream("server_testing.csv");

        // fixed pool, unlimited queue
        ExecutorService service = Executors.newFixedThreadPool(ThreadSize);
        ThreadPoolExecutor executor = (ThreadPoolExecutor) service;

        for(int i = 0; i<Iterations; i++) {
          Task t = new Task(i);
          executor.execute(t);
        }

        for(int j=0; j<FileArray.length; j++){
            new PrintStream(fout).println(FileArray[j] + ",");
        }
      }

  private static class Task implements Runnable {

    private int ID;

    public Task(int index) {
      this.ID = index;
    }

    public void run() {
        long Start = System.currentTimeMillis();

        int Size1 = 100000;
        int Size2 = 2 * Size1;
        int Size3 = Size1;

        byte[] list1 = new byte[Size1];
        byte[] list2 = new byte[Size2];
        byte[] list3 = new byte[Size3];

        for(int i=0; i<Size1; i++){
            list1[i] = myByte;
        }

        for (int i = 0; i < Size2; i=i+2)
        {
            list2[i] = myByte;
        }

        for (int i = 0; i < Size3; i++)
        {
            byte temp = list1[i];
            byte temp2 = list2[i];
            list3[i] = temp;
            list2[i] = temp;
            list1[i] = temp2;
        }

        long Finish = System.currentTimeMillis();
        long Duration = Finish - Start;
        TotalTime += Duration;
        FileArray[this.ID] = (int)Duration;
        System.out.println("Individual Time " + this.ID + " \t: " + (Duration) + " ms");


        if(Duration < FastestMemory){
            FastestMemory = Duration;
        }
        if (Duration > SlowestMemory)
        {
            SlowestMemory = Duration;
        }
    }
  }
}

person mezamorphic    schedule 04.04.2012    source источник
comment
Какая ОС? Windows? Я полагаю, что это, вероятно, так, если бы вы использовали Mono, вы бы, вероятно, сказали, но все же ...   -  person T.J. Crowder    schedule 04.04.2012
comment
Почему вы пытаетесь сравнить производительность на этих двух языках? Основная цель языка java - корпоративные приложения, если вам нужна производительность, просто используйте c ++.   -  person AlexTheo    schedule 04.04.2012
comment
Вы использовали клиентскую или серверную виртуальную машину для Java?   -  person fvu    schedule 04.04.2012
comment
Сколько ядер на самом деле использует ваше приложение?   -  person mellamokb    schedule 04.04.2012
comment
-Xmx 6000 - недопустимый параметр (не должно быть пробелов, а размер без суффикса указывается в байтах, должен быть кратен 1024 и быть больше 2 МБ), так что вы на самом деле используете?   -  person Mark Rotteveel    schedule 04.04.2012
comment
@fvu Я не знал, что есть разные версии. У меня есть виртуальная машина с 64-разрядной версией eclipse?   -  person mezamorphic    schedule 04.04.2012
comment
У вас есть серьезные проблемы с безопасностью потоков в ваших задачах (запись в статические переменные, потенциальная перезапись значений и / или наличие чередующейся записи при использовании long). Возможно, вы захотите обработать результаты отдельных задач после того, как они будут выполнены в основном потоке.   -  person Mark Rotteveel    schedule 04.04.2012
comment
@Mark, эта функция была просто проверкой возможностей памяти на нашем сервере. Мы полагаем, что у нас могут быть проблемы с пропускной способностью памяти. Наша библиотека написана на C #, поэтому мы сначала сравниваем ее с Java, чтобы избавиться от нее.   -  person mezamorphic    schedule 04.04.2012
comment
@mellamokb 4 физических ядра, 4x8 общих ядер   -  person mezamorphic    schedule 04.04.2012
comment
откуда вы взяли этот пул потоков Java? Возможно, имеет смысл попробовать другой или создать свои потоки самостоятельно, чтобы устранить любые возможные проблемы с ним.   -  person Christian    schedule 04.04.2012
comment
@Christian, я изменил код на приведенный выше (другой пример нашел в сети): bobah.net/d4d/source-code/misc/   -  person mezamorphic    schedule 04.04.2012
comment
Проблема @Mark напрямую влияет на ваше тестирование, потому что проблемы с безопасностью потоков связаны с фактическими переменными, которые вы используете для измерения производительности. Я обновил свой ответ ниже, чтобы решить эту проблему.   -  person sparc_spread    schedule 08.04.2012
comment
Вам нужно поставить executor.shutdown() в конце вашего кода, иначе ваша программа никогда не завершится.   -  person sparc_spread    schedule 08.04.2012


Ответы (4)


Резюме

Ниже представлены исходный ответ, обновление 1 и обновление 2. В обновлении 1 говорится о работе с условиями гонки вокруг переменных статистики теста с использованием структур параллелизма. Обновление 2 - это гораздо более простой способ решения проблемы состояния гонки. Надеюсь, больше никаких обновлений от меня - извините за длину ответа, но многопоточное программирование сложно!

Оригинальный ответ

Единственная разница между кодом заключается в том, что я использую приведенный ниже пул потоков

Я бы сказал, это огромная разница. Трудно сравнивать производительность двух языков, когда их реализации пула потоков представляют собой совершенно разные блоки кода, написанные в пользовательском пространстве. Реализация пула потоков может оказать огромное влияние на производительность.

Вам следует подумать об использовании собственных встроенных пулов потоков Java. См. ThreadPoolExecutor и весь java.util.concurrent пакет, частью которого он является. Класс Executors имеет удобные статические factory методы для пулов и является хорошим интерфейсом более высокого уровня. Все, что вам нужно, это JDK 1.5+, но чем новее, тем лучше. Решения fork / join, упомянутые в других плакатах, также являются частью этого пакета - как уже упоминалось, для них требуется 1.7+.

Обновление 1. Устранение условий гонки с использованием структур параллелизма.

У вас есть условия гонки вокруг настройки FastestMemory, SlowestMemory и TotalTime. Для первых двух вы выполняете < и > тестирование, а затем настройку более чем за один шаг. Это не атомарно; безусловно, существует вероятность, что другой поток обновит эти значения в промежутке между тестированием и настройкой. Параметр += для TotalTime также не атомарен: это тест и замаскированный набор.

Вот несколько предлагаемых исправлений.

TotalTime

Цель здесь - потокобезопасный, атомарный += из TotalTime.

// At the top of everything
import java.util.concurrent.atomic.AtomicLong;  

...    

// In PoolDemo
static AtomicLong TotalTime = new AtomicLong();    

...    

// In Task, where you currently do the TotalTime += piece
TotalTime.addAndGet (Duration); 

FastestMemory / SlowestMemory

Целью здесь является тестирование и обновление FastestMemory и SlowestMemory каждого на атомарном этапе, чтобы ни один поток не мог проскользнуть между этапами тестирования и обновления, чтобы вызвать состояние гонки.

Самый простой подход:

Защитите тестирование и настройку переменных, используя сам класс в качестве монитора. Нам нужен монитор, содержащий переменные, чтобы гарантировать синхронизированную видимость (спасибо @ A.H. За это). Мы должны использовать сам класс, потому что все static.

// In Task
synchronized (PoolDemo.class) {
    if (Duration < FastestMemory) {
        FastestMemory = Duration;
    }

    if (Duration > SlowestMemory) {
        SlowestMemory = Duration;
    }
}

Промежуточный подход:

Возможно, вам не понравится использовать весь класс для монитора или выставлять монитор с помощью класса и т. Д. Вы можете создать отдельный монитор, который сам не содержит FastestMemory и SlowestMemory, но тогда вы столкнетесь с проблемами видимости синхронизации. Вы можете обойти это, используя ключевое слово volatile.

// In PoolDemo
static Integer _monitor = new Integer(1);
static volatile long FastestMemory = 2000000;
static volatile long SlowestMemory = 0;

...

// In Task
synchronized (PoolDemo._monitor) {
    if (Duration < FastestMemory) {
        FastestMemory = Duration;
    }

    if (Duration > SlowestMemory) {
        SlowestMemory = Duration;
    }
}

Расширенный подход:

Здесь мы используем классы java.util.concurrent.atomic вместо мониторов. В условиях жесткой конкуренции этот подход должен работать лучше, чем подход synchronized. Попробуйте и убедитесь.

// At the top of everything
import java.util.concurrent.atomic.AtomicLong;    

. . . . 

// In PoolDemo
static AtomicLong FastestMemory = new AtomicLong(2000000);
static AtomicLong SlowestMemory = new AtomicLong(0);

. . . . .

// In Task
long temp = FastestMemory.get();       
while (Duration < temp) {
    if (!FastestMemory.compareAndSet (temp, Duration)) {
        temp = FastestMemory.get();       
    }
}

temp = SlowestMemory.get();
while (Duration > temp) {
    if (!SlowestMemory.compareAndSet (temp, Duration)) {
        temp = SlowestMemory.get();
    }
}

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

Изначально я разместил это обновление как комментарий, но переместил его сюда, чтобы у меня было место для показа кода. Это обновление прошло несколько итераций - благодаря A.H. за обнаружение ошибки, которая была у меня в более ранней версии. Все, что есть в этом обновлении, заменяет все, что указано в комментарии.

И последнее, но не менее важное: отличный источник, охватывающий весь этот материал, - Java. Параллелизм на практике, лучшая книга по параллелизму в Java и одна из лучших книг по Java в целом.

Обновление 2.Управление условиями гонки гораздо более простым способом.

Недавно я заметил, что ваш текущий код никогда не завершится, если вы не добавите executorService.shutdown(). То есть, потоки, не являющиеся демонами, живущие в этом пуле, должны быть завершены, иначе основной поток никогда не завершится. Это заставило меня подумать, что, поскольку мы должны ждать завершения всех потоков, почему бы не сравнить их продолжительность после их завершения и, таким образом, полностью обойти одновременное обновление FastestMemory и т. Д.? Это проще и может быть быстрее; больше нет блокировок или накладных расходов CAS, и вы в любом случае уже делаете итерацию FileArray в конце.

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

При этом вы вносите следующие изменения:

// In PoolDemo
// This part is the same, just so you know where we are
for(int i = 0; i<Iterations; i++) {
    Task t = new Task(i);
    executor.execute(t);
}

// CHANGES BEGIN HERE
// Will block till all tasks finish. Required regardless.
executor.shutdown();
executor.awaitTermination(10, TimeUnit.SECONDS);

for(int j=0; j<FileArray.length; j++){
    long duration = FileArray[j];
    TotalTime += duration;

    if (duration < FastestMemory) {
        FastestMemory = duration;
    }

    if (duration > SlowestMemory) {
        SlowestMemory = duration;
    }

    new PrintStream(fout).println(FileArray[j] + ",");
}

. . . 

// In Task
// Ending of Task.run() now looks like this
long Finish = System.currentTimeMillis();
long Duration = Finish - Start;
FileArray[this.ID] = (int)Duration;
System.out.println("Individual Time " + this.ID + " \t: " + (Duration) + " ms");

Попробуйте и этот подход.

Вы обязательно должны проверить свой код C # на наличие аналогичных условий гонки.

person sparc_spread    schedule 04.04.2012
comment
Я только что использовал службу Executor, и у меня все те же сроки. Я основал свой код на этом: bobah.net/d4d / исходный код / ​​разное / - person mezamorphic; 04.04.2012
comment
У вас есть условия гонки вокруг настройки FastestMemory, SlowestMemory и TotalTime. Для первых двух вы выполняете < и > тестирование, а затем выполняете более одного шага настройки. Это не атомарно, есть вероятность, что другой поток обновит эти значения в промежутке между тестированием и настройкой. Так что заверните < тест и настройку в synchronized блок - один для самого быстрого и один для самого медленного. Между тем, установка += для TotalTime также не атомарна. Либо synchronize эту операцию, либо попробуйте AtomicLong. - person sparc_spread; 08.04.2012
comment
Использование synchronized (PoolDemo.monitor) сделает две вещи: Во-первых: вы сериализуете потоки - это нормально и задумано. Во-вторых: JVM гарантирует, что при выходе из синхронизированного блока новое состояние монитора будет видно другим ЦП. Нет гарантии, что другие переменные будут видны другим процессорам. В вашем случае FastestMemory & co не дает никаких гарантий. Вы должны либо синхронизировать их, либо вставить их в объект монитора, либо использовать для них AtomicXYZ. - person A.H.; 08.04.2012
comment
Ах, ты абсолютно прав. Это все потому, что я хотел избежать синхронизации самого класса (поскольку все равно static). Решение для синхронизации также могло бы работать, если бы я сделал их volatile - это решило бы проблему видимости. В любом случае, пора редактировать мой пост - спасибо, что поймали это! - person sparc_spread; 08.04.2012
comment
@sparc_spread: Что касается обновления 2: еще более простым решением было бы отказаться от Runnable, вместо этого использовать Callable, который возвращает все соответствующие данные. Это устраняет всю синхронизацию в коде пользователя. - person A.H.; 10.04.2012

... но Java иногда занимает 2000 мс ...

А также

    byte[] list1 = new byte[Size1];
    byte[] list2 = new byte[Size2];
    byte[] list3 = new byte[Size3];

Это будет сборщик мусора, очищающий ваши массивы. Если вы действительно хотите настроить это, я предлагаю вам использовать какой-то кеш для массивов.

Изменить

Вот этот

   System.out.println("Individual Time " + this.ID + " \t: " + (Duration) + " ms");

выполняет один или несколько synchronized внутри. Таким образом, ваш высоко «параллельный» код на этом этапе будет довольно хорошо сериализован. Просто удалите его и повторите попытку.

person A.H.    schedule 08.04.2012
comment
+1 Согласен, что это могло быть фактором. Одним из интересных экспериментов было бы изменение list* переменных из локальных переменных run() в переменные экземпляра Task. Затем сохраните каждый Task t в List при его создании. Цель здесь - сохранить list* блоков в ссылочном графе до самого конца программы. Это означало бы, что их не будет gc до конца. Было бы интересно посмотреть, станут ли времена в этот момент более детерминированными. Конечно, вам также нужно убедиться, что куча достаточно велика для этого. - person sparc_spread; 08.04.2012
comment
@sparc_spread Я ожидал, что GC пытается освободить некоторую память, постоянно терпит неудачу, а затем выделяет новую RAM из ОС. Это точно не самый быстрый способ. Итак, какую ценность может предоставить такая точка данных? ИМО нет. - person A.H.; 08.04.2012
comment
Было бы интересно провести различие между стоимостью фактического успешного сбора и стоимостью описанного вами взлома, что, я согласен, вероятно, начнется в сценарии, который я создал. - person sparc_spread; 08.04.2012

Хотя ответ @sparc_spread великолепен, я заметил еще одну вещь:

Запускаю приложение с 1000 итерациями функции

Обратите внимание, что JVM HotSpot работает в интерпретируемом режиме для первых 1,5 тыс. Итераций любой функции в клиентском режиме и для 10 тыс. Итераций в режиме сервера. Компьютеры с таким количеством ядер автоматически считаются «серверами» JVM HotSpot.

Это означало бы, что C # будет выполнять JIT (и работать в машинном коде) раньше, чем Java, и имеет шанс на повышение производительности во время выполнения функции. Попробуйте увеличить количество итераций до 20 000 и начните отсчет с 10 000 итераций.

Обоснованием здесь является то, что JVM собирает статистические данные о том, как лучше всего выполнять JIT. Он надеется, что ваша функция будет выполняться много времени, поэтому для более быстрого выполнения в целом требуется механизм «медленной начальной загрузки». Или, по их словам, «20% функций работают 80% времени», так зачем же JIT их всех?

person Aviad Ben Dov    schedule 08.04.2012
comment
Означает ли это, что функция, которая выполняется только один раз, но имеет жесткий цикл с миллионами итераций, не получает JIT? Это звучит странно. - person CodesInChaos; 08.04.2012
comment
+1 Я думаю, вы тут кое-что поняли. Всегда хорошо проводить разогрев с такого рода тестами, а с описанным вами поведением JIT это кажется важным. Я почти уверен, что ваш ответ + ответ @AH о gc поведении объясняет большую часть того, что происходит. Я просто пытаюсь исправить условия гонки, но не уверен, что они являются причиной того, что описывает оригинальный плакат. - person sparc_spread; 08.04.2012
comment
@CodeInChaos, это на самом деле очень осознанный выбор. JIT стоит дорого; следовательно, они делают это для 20% кода, который выполняется (весь этот код начальной загрузки? Нет необходимости его JIT). Кроме того, поскольку они не торопятся, у них есть возможность проверить статистику использования функции, а еще лучше - JIT. - person Aviad Ben Dov; 10.04.2012
comment
@AviadBenDov Я не критиковал основную идею, но считаю, что детализация метода слишком грубая. Было бы разумнее использовать JIT для метода после того, как фрагмент кода был выполнен n раз, а не после того, как метод был вызван n раз. Остальные методы, которые содержат замкнутый цикл (на выполнение которого требуется много времени), но вызываются только несколько раз, не будут подвергнуты JIT-обработке. - person CodesInChaos; 10.04.2012
comment
@CodeInChaos Теперь я понимаю, что вы имеете в виду. Вы правы в этом, но насколько я понимаю, JIT может выполняться только для целых методов - следовательно, ограничение. Однако, на мой взгляд, я могу только представить плохо сформированный код, когда пытаюсь представить ситуацию, когда 80% времени приложения выполняется одним методом с очень длинным циклом и без вызовов методов внутри цикла .. :-) - person Aviad Ben Dov; 13.04.2012

Вы используете java6? В Java 7 есть функции для повышения производительности при параллельном программировании:

http://www.oracle.com/technetwork/articles/java/fork-join-422606.html

person Oscar Castiblanco    schedule 04.04.2012
comment
Я просто собираюсь быстро установить версию 7 - person mezamorphic; 04.04.2012
comment
С java 7 вы можете использовать платформу fork для повышения производительности. Здесь есть поток stackoverflow, который говорит об этом stackoverflow.com/questions/7926864/ - person Oscar Castiblanco; 04.04.2012
comment
Текущая версия на веб-сайте - 1.7.0_3, но она могла быть довольно недавней (я думаю, что в более ранних версиях java 7 было несколько проблем, поэтому они, возможно, не выставили их для прямой загрузки). - person Roman A. Taycher; 04.04.2012