Я использую BlockingQueue: s (пытается как ArrayBlockingQueue, так и LinkedBlockingQueue) передавать объекты между различными потоками в приложении, в котором я сейчас работаю. Производительность и латентность относительно важны в этом приложении, поэтому мне было любопытно, сколько времени требуется для передачи объектов между двумя потоками с помощью BlockingQueue. Чтобы измерить это, я написал простую программу с двумя потоками (один потребитель и один производитель), где я разрешаю производителю передавать временную метку (полученную с использованием System.nanoTime()) для потребителя, см. Код ниже.Java BlockingQueue latency high на Linux
Я помню, как читал где-то на каком-то форуме, что потребовалось около 10 микросекунд для кого-то другого, кто это пробовал (не знаю, на какой ОС и аппаратном обеспечении было), поэтому я не был слишком удивлен, когда потребовалось ~ 30 микросекунд для меня на моем окне Windows 7 (процессор Intel E7500 Core 2 Duo, 2,93 ГГц), в то время как работает много других приложений в фоновом режиме. Тем не менее, я был очень удивлен, когда я сделал тот же тест на нашем гораздо более быстром сервере Linux (два четырехъядерных процессора Intel X5677 3.46GHz, работающих под управлением Debian 5 с ядром 2.6.26-2-amd64). Я ожидал, что латентность будет ниже, чем у моего окна, но, наоборот, она была намного выше - ~ 75 - 100 микросекунд! Оба теста были выполнены с помощью Sun's Hotspot JVM версии 1.6.0-23.
Проводили ли какие-либо аналогичные тесты с аналогичными результатами в Linux? Или кто-нибудь знает, почему он намного медленнее в Linux (с лучшим оборудованием), может быть, что переключение потоков просто намного медленнее в Linux по сравнению с Windows? Если это так, кажется, что окна действительно намного лучше подходят для некоторых приложений. Любая помощь, помогающая мне понять относительно высокие показатели, очень ценится.
Edit:
После комментария от DaveC, я также сделал тест, где я ограничило JVM (на машине Linux) на одном ядре (т.е. все темы, работающие на том же ядре). Это резко изменило результаты - латентность снизилась до менее 20 микросекунд, то есть лучше, чем результаты на машине Windows. Я также провел несколько тестов, в которых я ограничил поток производителей одним ядром и потребительским потоком на другой (пытаясь как иметь их в одном и том же сокете и в разных сокетах), но это, похоже, не помогло - латентность все еще была ~ 75 микросекунд. Кстати, это тестовое приложение - это почти все, что я запускаю на машине во время тестирования.
Кто-нибудь знает, имеют ли эти результаты смысл? Должно ли быть действительно намного медленнее, если производитель и потребитель работают на разных ядрах? Любой вход действительно оценен.
Edited снова (6 января):
Я экспериментировал с различными изменениями в коде и работает среды:
Я обновил ядро Linux 2.6.36.2 с (от 2.6.26.2). После обновления ядра измеренное время изменилось на 60 микросекунд с очень небольшими вариациями, начиная с 75-100 до обновления. Настройка близости процессора к потоку производителя и потребителя не имела никакого эффекта, за исключением случаев, когда они ограничивали их одним ядром. При работе на одном и том же ядре измеряемая латентность составляла 13 микросекунд.
В исходном коде у меня был продюсер, который спал в течение 1 секунды между каждой итерацией, чтобы дать потребителю достаточно времени, чтобы вычислить прошедшее время и распечатать его на консоли. Если я удалю вызов Thread.sleep() и вместо этого позволю как барьер производителя, так и потребительский вызов.await() на каждой итерации (потребитель называет его после печати прошедшего времени на консоль), измеренная задержка уменьшается с 60 микросекунд до менее 10 микросекунд. При запуске потоков на одном и том же ядре латентность становится ниже 1 микросекунды. Может ли кто-нибудь объяснить, почему это значительно сократило латентность?Мое первое предположение заключалось в том, что изменение привело к тому, что продюсер назвал queue.put() перед тем, как потребитель назвал queue.take(), поэтому потребителю никогда не приходилось блокировать, но после игры с модифицированной версией ArrayBlockingQueue я обнаружил это предположение было ложным - потребитель действительно блокировал. Если у вас есть другие предположения, пожалуйста, дайте мне знать. (Кстати, если я позволю продюсеру вызвать как Thread.sleep(), так и барьер.await(), латентность остается на 60 микросекунд).
Я также пробовал другой подход - вместо вызова queue.take() я вызывал queue.poll() с тайм-аутом в 100 микронов. Это уменьшило среднюю задержку до менее 10 микросекунд, но, конечно, намного интенсивнее процессора (но, вероятно, менее интенсивный процессор, ожидающий ожидание?).
Отредактировано раз (10 января) - Проблема решена:
ninjalj предположил, что задержка ~ 60 мкс было связано с CPU того, чтобы проснуться от более глубоких состояний сна - и он был совершенно прав! После отключения C-состояний в BIOS латентность была уменьшена до < 10 микросекунд. Это объясняет, почему я получил гораздо лучшую задержку в пункте 2 выше - когда я отправлял объекты чаще, процессор был достаточно занят, чтобы не переходить в более глубокие состояния сна. Большое спасибо всем, кто нашел время, чтобы прочитать мой вопрос и поделился своими мыслями!
...
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.CyclicBarrier;
public class QueueTest {
ArrayBlockingQueue<Long> queue = new ArrayBlockingQueue<Long>(10);
Thread consumerThread;
CyclicBarrier barrier = new CyclicBarrier(2);
static final int RUNS = 500000;
volatile int sleep = 1000;
public void start() {
consumerThread = new Thread(new Runnable() {
@Override
public void run() {
try {
barrier.await();
for(int i = 0; i < RUNS; i++) {
consume();
}
} catch (Exception e) {
e.printStackTrace();
}
}
});
consumerThread.start();
try {
barrier.await();
} catch (Exception e) { e.printStackTrace(); }
for(int i = 0; i < RUNS; i++) {
try {
if(sleep > 0)
Thread.sleep(sleep);
produce();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public void produce() {
try {
queue.put(System.nanoTime());
} catch (InterruptedException e) {
}
}
public void consume() {
try {
long t = queue.take();
long now = System.nanoTime();
long time = (now - t)/1000; // Divide by 1000 to get result in microseconds
if(sleep > 0) {
System.out.println("Time: " + time);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
QueueTest test = new QueueTest();
System.out.println("Starting...");
// Run first once, ignoring results
test.sleep = 0;
test.start();
// Run again, printing the results
System.out.println("Starting again...");
test.sleep = 1000;
test.start();
}
}
вы пробовали тест на коробке linux, ограничивая jvm только одним процессором? может помочь определить, где идет время. – DaveC
Интересно. Я попытался ограничить его конкретным процессором, запустив приложение с помощью команды «taskset 0x00000001 java QueueTest», и латентность была уменьшена примерно с 75-100 до ~20 микросекунд! Я не уверен, что понимаю, хотя ... – Johan
@Johan: Эти времена вы сообщаете одинаково во многих итерациях? CyclicBarrier используется для координации потоков, работающих над независимыми задачами. Ваши задачи, хотя и не являются независимыми. У вас есть как производитель, так и потребитель ждет на барьере, а затем (как только оба потока достигнут барьерной точки), они, по сути, начинают синхронизацию в блокирующей очереди. Вы могли видеть перемежения всех видов комбинаций планирования, описывающих различные задержки. – Cratylus