Почему Java не использует все мои ядра процессора эффективно

Я запускаю Ubuntu на машине с четырехъядерным процессором. Я написал некоторый тестовый Java-код, который порождает определенное количество процессов, которые просто увеличивают изменчивую переменную для заданного количества итераций при запуске.

Я ожидал бы, что время выполнения не увеличится значительно, а количество потоков меньше или равно числу ядер, т.е. 4. Фактически, это время, когда я получаю "реальное время" из UNIX time:

1 thread: 1.005s

2 потока: 1.018s

3 темы: 1.528s

4 потока: 1.982s

5 нитей: 2.479s

6 нитей: 2.934s

7 потоков: 3.356s

8 нитей: 3.793s

Это показывает, что добавление одного дополнительного потока не увеличивает время, как ожидалось, но тогда время увеличивается с 3 и 4 потоками.

Сначала я думал, что это может быть связано с тем, что ОС препятствует использованию JVM всех ядер, но я побежал top, и он ясно показал, что с 3 потоками 3 ядра работали на ~ 100%, а с 4 потока, 4 ядра были превышены.

Мой вопрос: почему код работает на 3/4 процессорах не примерно такой же, как при работе на 1/2? Поскольку он работает параллельно на всех ядрах.

Вот мой основной метод для справки:

class Example implements Runnable {

    // using this so the compiler does not optimise the computation away
    volatile int temp;

    void delay(int arg) {
        for (int i = 0; i < arg; i++) {
            for (int j = 0; j < 1000000; j++) {
                this.temp += i + j;
            }
        }
    }

    int arg;
    int result;

    Example(int arg) {
        this.arg = arg;
    }

    public void run() {
        delay(arg);
        result = 42;
    }

    public static void main(String args[]) {

        // Get the number of threads (the command line arg)

        int numThreads = 1;
        if (args.length > 0) {
            try {
                numThreads = Integer.parseInt(args[0]);
            } catch (NumberFormatException nfe) {
                System.out.println("First arg must be the number of threads!");
            }
        }

        // Start up the threads

        Thread[] threadList = new Thread[numThreads];
        Example[] exampleList = new Example[numThreads];
        for (int i = 0; i < numThreads; i++) {
            exampleList[i] = new Example(1000);
            threadList[i] = new Thread(exampleList[i]);
            threadList[i].start();
        }

        // wait for the threads to finish

        for (int i = 0; i < numThreads; i++) {
           try {
                threadList[i].join();
                System.out.println("Joined with thread, ret=" + exampleList[i].result);
            } catch (InterruptedException ie) {
                System.out.println("Caught " + ie);
            }
        }
    }
}

Ответы

Ответ 1

Core i5 в Lenovo X1 Carbon не является четырехъядерным процессором. Это двухъядерный процессор с гиперпотоком. Когда вы выполняете только тривиальные операции, которые не приводят к частым длинным конвейерным стойкам, тогда планировщик гиперпотоков не будет иметь большой возможности перетащить другие операции в заторможенный конвейер, и вы не увидите производительность, эквивалентную четырем реальным ядрам.

Ответ 2

Использование нескольких ЦП помогает до такой степени, что вы насыщаете некоторый базовый ресурс.

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

Попробуйте получить доступ к кэшу L1 меньше

public class Example implements Runnable {
    // using this so the compiler does not optimise the computation away
    volatile int temp;

    void delay(int arg) {
        for (int i = 0; i < arg; i++) {
            int temp = 0;
            for (int j = 0; j < 1000000; j++) {
                temp += i + j;
            }
            this.temp += temp;
        }
    }

    int arg;
    int result;

    Example(int arg) {
        this.arg = arg;
    }

    public void run() {
        delay(arg);
        result = 42;
    }

    public static void main(String... ignored) {

        int MAX_THREADS = Integer.getInteger("max.threads", 8);
        long[] times = new long[MAX_THREADS + 1];
        for (int numThreads = MAX_THREADS; numThreads >= 1; numThreads--) {
            long start = System.nanoTime();

            // Start up the threads

            Thread[] threadList = new Thread[numThreads];
            Example[] exampleList = new Example[numThreads];
            for (int i = 0; i < numThreads; i++) {
                exampleList[i] = new Example(1000);
                threadList[i] = new Thread(exampleList[i]);
                threadList[i].start();
            }

            // wait for the threads to finish

            for (int i = 0; i < numThreads; i++) {
                try {
                    threadList[i].join();
                    System.out.println("Joined with thread, ret=" + exampleList[i].result);
                } catch (InterruptedException ie) {
                    System.out.println("Caught " + ie);
                }
            }
            long time = System.nanoTime() - start;
            times[numThreads] = time;
            System.out.printf("%d: %.1f ms%n", numThreads, time / 1e6);
        }
        for (int i = 2; i <= MAX_THREADS; i++)
            System.out.printf("%d: %.3f time %n", i, (double) times[i] / times[1]);
    }
}

На моем двухъядерном, гиперпоточном ноутбуке он создается в форме threads: factor

2: 1.093 time 
3: 1.180 time 
4: 1.244 time 
5: 1.759 time 
6: 1.915 time 
7: 2.154 time 
8: 2.412 time 

по сравнению с первоначальным тестом

2: 1.092 time 
3: 2.198 time 
4: 3.349 time 
5: 3.079 time 
6: 3.556 time 
7: 4.183 time 
8: 4.902 time 

Общим ресурсом для более эффективного использования является кеш L3. Это распределяется между центральными процессорами, и хотя он допускает степень concurrency, он не масштабируется значительно выше, чем у процессоров. Я предлагаю вам проверить, что делает ваш пример кода, и убедиться, что они могут работать независимо и не использовать общие ресурсы. например Большинство чипов имеют ограниченное количество FPU.

Ответ 3

Есть несколько вещей, которые могут ограничить, насколько эффективно вы можете многопоточно использовать приложение.

  • Насыщенность ресурса, такого как пропускная способность памяти/шины/etc.

  • Проблемы с блокировкой/конфликтом (например, если потокам постоянно приходится ждать друг друга, чтобы закончить).

  • Другие процессы, запущенные в системе.

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

Попробуйте переключить каждый поток, чтобы работать на собственной части данных без переменной volatile. Это должно уменьшить все формы разногласий.

Ответ 4

Если вы используете это на Core i5 (столько, сколько Google сообщает мне о Lenovo X1 Carbon), то у вас есть двухъядерная машина с двумя гипер-ядрами. Отчеты i5 для ОС - и, следовательно, для Java - как четырехъядерные, поэтому гиперядро используется как реальные ядра, но все это делается для ускорения переключения потоков.

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

Ответ 5

Уже есть два хороших ответа, оба прекрасно объясняют, что происходит.

Посмотрите на процессор, большая часть "четырехъядерного ядра" от Intel на самом деле является двухъядерным процессором, который имитирует четырехъядерный ядро ​​ОС (да, они говорят вам, что у вас есть 4 ядра, но у вас всего 2 факт...). Это лучшее объяснение вашей проблемы, потому что время увеличивается как двухъядерный процессор.

Если у вас есть реальный 4 ядра, другой ответ заключается в том, что у вас есть код concurrency.