Я использую BlockingQueue: s (пытается как ArrayBlockingQueue, так и LinkedBlockingQueue) передавать объекты между разными потоками в приложении Im, которое работает в настоящее время. Производительность и латентность относительно важны в этом приложении, поэтому мне было любопытно, сколько времени требуется для передачи объектов между двумя потоками с помощью BlockingQueue. Чтобы измерить это, я написал простую программу с двумя потоками (один потребитель и один производитель), где я разрешаю производителю передавать временную метку (полученную с использованием System.nanoTime()) для потребителя, см. Код ниже.
Я помню, как читал где-то на каком-то форуме, что потребовалось около 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 микросекунд! Оба теста были выполнены с помощью Suns Hotspot JVM версии 1.6.0-23.
Кто-нибудь еще пробовал подобные тесты с аналогичными результатами в Linux? Или кто-нибудь знает, почему он намного медленнее в Linux (с лучшим оборудованием), может быть, что переключение потоков просто намного медленнее в Linux по сравнению с Windows? Если это так, то похоже, что окна на самом деле намного лучше подходят для некоторых приложений. Любая помощь, помогающая мне понять относительно высокие показатели, очень ценится.
Изменить
После комментария от DaveC я также проверил, где я ограничил JVM (на машине Linux) одним ядром (т.е. Все потоки, запущенные на одном ядре). Это резко изменило результаты - латентность снизилась до менее 20 микросекунд, то есть лучше, чем результаты на машине Windows. Я также провел несколько тестов, в которых я ограничил поток производителей одним ядром и потребительским потоком на другой (пытаясь как иметь их в одном и том же сокете и в разных сокетах), но это, похоже, не помогло - латентность все еще была ~ 75 микросекунд. Btw, это тестовое приложение - это почти все, что я запускаю на машине во время теста на выполнение.
Кто-нибудь знает, имеют ли эти результаты смысл? Должно ли быть действительно намного медленнее, если производитель и потребитель работают на разных ядрах? Любой вход действительно оценен.
Отредактировано снова (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 микросекунд была вызвана тем, что ЦП должен был проснуться от более глубоких состояний сна - и он был совершенно прав! После отключения 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();
}
}