Анализ Runtime.availableProcessors()

Java

Я видел статью недавноDocker больше не будет смущаться Java: в Java 10 сделали специальные оптимизации для Docker, в котором упоминалось, что в java10 были сделаны некоторые специальные оптимизации для докера. Как мы все знаем, поддержка контейнеризации докеров в java всегда вызывала смущение, поскольку нижний уровень докера использует контрольные группы для изоляции на уровне процесса, хотя мы устанавливаем лимиты ресурсов контейнера через докер, виртуальная машина JVM не воспринимает эти ограничения. Например, у нашего хоста может быть 8 ядер и 16 ГБ, а контейнер докера ограничен 2 ядрами и 4 ГБ. Ресурсы, считываемые в контейнере, могут по-прежнему иметь 8 ядер и 16 ГБ. Обычно мы можем считывать ресурсы компьютера для оптимизации производительности, например количество потоков ядра. , настройка максимального количества потоков. Для некоторых программ запуск в докере может привести к потере производительности, к счастью, в java10 добавлена ​​такая поддержка, и есть планы по совместимости с jdk8.

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

Какие функции предоставляет availableProcessors?

/**
     * Returns the number of processors available to the Java virtual machine.
     *
     * <p> This value may change during a particular invocation of the virtual
     * machine.  Applications that are sensitive to the number of available
     * processors should therefore occasionally poll this property and adjust
     * their resource usage appropriately. </p>
     *
     * @return  the maximum number of processors available to the virtual
     *          machine; never smaller than one
     * @since 1.4
     */
    public native int availableProcessors();

Это написано в документе jdk, вернитеКоличество доступных ядер виртуальной машины jvm. И после него примечание:Это значение может измениться во время конкретного вызова виртуальной машины.. Наше обычное интуитивное впечатление об этой функции таково:Возвращает количество ЦП машины, это должно быть постоянное значение. С этой точки зрения могут возникнуть большие недоразумения. Отсюда у меня два вопроса:

  • 1. Какое количество доступных ядер в JVM?
  • 2. Почему возвращаемое значение является переменным? Как это работает?

Количество ядер, доступных для JVM

Это легко понять, как следует из названия, количество ядер ЦП, которые JVM может использовать для работы. На многоядерном ЦП-сервере может быть установлено несколько приложений, и JVM — это только одна его часть, а некоторые ЦП используются другими приложениями.

Почему возвращаемое значение изменчиво? Как это работает?

Возвращаемое значение переменной также легко понять.Поскольку несколько приложений совместно используют ЦП на многоядерном ЦП-сервере, количество, которое может быть использовано JVM в разное время, конечно, отличается.В этом случае, как это делается в java ? При чтении исходного кода jdk8 все еще существует большая разница между реализацией системы Linux и системы Windows.

linux 实现
int os::active_processor_count() {
  // Linux doesn't yet have a (official) notion of processor sets,
  // so just return the number of online processors.
  int online_cpus = ::sysconf(_SC_NPROCESSORS_ONLN);
  assert(online_cpus > 0 && online_cpus <= processor_count(), "sanity check");
  return online_cpus;
}

Реализация Linux относительно ленива, и системные параметры считываются напрямую через sysconf, _SC_NPROCESSORS_ONLN.

windows 实现
int os::active_processor_count() {
  DWORD_PTR lpProcessAffinityMask = 0;
  DWORD_PTR lpSystemAffinityMask = 0;
  int proc_count = processor_count();
  if (proc_count <= sizeof(UINT_PTR) * BitsPerByte &&
      GetProcessAffinityMask(GetCurrentProcess(), &lpProcessAffinityMask, &lpSystemAffinityMask)) {
    // Nof active processors is number of bits in process affinity mask
    int bitcount = 0;
    while (lpProcessAffinityMask != 0) {
      lpProcessAffinityMask = lpProcessAffinityMask & (lpProcessAffinityMask-1);
      bitcount++;
    }
    return bitcount;
  } else {
    return proc_count;
  }
}

Реализация системы Windows более сложна.Видно, что необходимо не только судить о том, доступен ли ЦП, но и судить о том, может ли поток использовать ЦП в соответствии с привязкой к ЦП. Он использует цикл while для синтаксического анализа маски привязки ЦП, поэтому это операция с интенсивным использованием ЦП.

Тестирование производительности

Благодаря приведенному выше анализу мы можем в основном понять, что эта операция чувствительна к процессору, так как же ее производительность работает в различных операционных системах? Таким образом, я протестировал некоторые характеристики этой функции в нормальных условиях работы и при полной загрузке процессора. Тестовые данные должны выполнить 1 миллион вызовов, подсчитать 10 выполнений и взять среднее значение. Соответствующий код выглядит следующим образом:

public class RuntimeDemo {

    private static final int EXEC_TIMES = 100_0000;
    private static final int TEST_TIME = 10;

    public static void main(String[] args) throws Exception{
        int[] arr = new int[TEST_TIME];
        for(int i = 0; i < TEST_TIME; i++){
            long start = System.currentTimeMillis();
            for(int j = 0; j < EXEC_TIMES; j++){
                Runtime.getRuntime().availableProcessors();
            }
            long end = System.currentTimeMillis();
            arr[i] = (int)(end-start);
        }

        double avg = Arrays.stream(arr).average().orElse(0);
        System.out.println("avg spend time:" + avg + "ms");

    }
}

Код полной загрузки процессора выглядит следующим образом:

public class CpuIntesive {

    private static final int THREAD_COUNT = 16;

    public static void main(String[] args) {
        for(int i = 0; i < THREAD_COUNT; i++){
            new Thread(()->{
                long count = 1000_0000_0000L;
                long index=0;
                long sum = 0;
                while(index < count){
                    sum = sum + index;
                    index++;
                }
            }).start();
        }
    }
}
система настроить Методы испытаний Результаты теста
Windows 2 ядра 8G нормальный 1425.2ms
Windows 2 ядра 8G Полная загрузка процессора 6113.1ms
MacOS 4 ядра 8G нормальный 69.4ms
MacOS 4 ядра 8G Полная загрузка процессора 322.8ms

Хотя конфигурация двух машин сильно различается, сравнение тестовых данных не имеет смысла, но из тестовой ситуации можно сделать следующие выводы:

  • Производительность windows и linux-подобных систем сильно различается, что связано со спецификой реализации
  • Вычисления, интенсивно использующие ЦП, оказывают большее влияние на производительность этой функции.
  • В целом производительность этой функции вполне приемлемая, а максимальное время составляет всего 6 мкс при полной загрузке ЦП Windows. Его можно понизить до уровня ns в системе Linux.

Суммировать

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

благодарный