Почему вывод символа 'B' в Java-цикле намного медленнее, чем вывод символа '#'
Краткий ответ: дело не в Java, а в терминале
Проблема, с которой вы столкнулись, — классический пример того, как внешние факторы (а не сама Java или JIT-компиляция) влияют на производительность ввода-вывода. Ваш код на самом деле работает с одинаковой скоростью для обоих символов. Замедление возникает на стороне терминала/консоли, которая обрабатывает вывод.
Почему 'B' медленнее '#'?
Всё дело в переносе строки (line wrapping):
- Символ '#' — это узкий символ. В консоли Windows (cmd.exe) или терминалах *nix он не вызывает перенос строки, даже если вы выводите 1000 символов подряд. Терминал просто рисует их в одну строку.
- Символ 'B' — это широкий символ (особенно в шрифтах с засечками или в некоторых кодировках). В большинстве терминалов при достижении правого края окна происходит автоматический перенос строки (word wrap). Терминал вынужден пересчитывать позицию курсора, скроллить содержимое и перерисовывать строки.
Экспериментальное подтверждение
Чтобы убедиться, выполните три модификации вашего кода:
// Тест 1: Вывод 'B' с явным переводом строки (должно стать быстрее)
for (int i = 0; i < 1000; i++) {
System.out.println("B"); // Каждый символ на новой строке — никакого переноса!
}
// Тест 2: Вывод 'B' в одну строку, но с увеличением ширины консоли
// Сделайте окно терминала очень широким (например, 200 символов) — скорость вырастет
// Тест 3: Вывод '#' в узком окне (сделайте окно шириной 20 символов)
// Теперь '#' будет медленнее, так как начнётся перенос строки
Что происходит на уровне JVM?
System.out.print() использует буферизированный поток PrintStream. По умолчанию он автоматически сбрасывает буфер (autoflush) при каждом символе новой строки '\n'. Но в вашем коде вы используете print() без '\n', поэтому буфер не сбрасывается принудительно. Однако:
- JVM отправляет данные в буфер операционной системы (stdout).
- ОС передаёт их терминалу.
- Терминал обрабатывает отображение.
Задержка возникает именно на последнем этапе. Терминал — это отдельный процесс, и его производительность зависит от:
- Ширины окна
- Алгоритма переноса строк (может быть медленным в старых терминалах)
- Шрифта и размера символов (глиф 'B' сложнее для рендеринга, чем '#')
Как исправить?
Если вам нужно вывести много символов в одну строку, используйте StringBuilder для формирования строки и один вызов System.out.print():
StringBuilder sb = new StringBuilder(1000);
for (int i = 0; i < 1000; i++) {
sb.append("B");
}
System.out.print(sb.toString()); // Один системный вызов — мгновенно
Или используйте BufferedWriter с FileDescriptor.out для прямого доступа к stdout:
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out));
for (int i = 0; i < 1000; i++) {
writer.write('B');
}
writer.flush(); // Теперь всё быстро
Вывод
Это не баг Java, а особенность работы терминала. Вопрос на Stack Overflow был очень популярен именно потому, что многие начинающие разработчики путают производительность кода с производительностью среды выполнения (терминала). Всегда учитывайте, что System.out — это медленный I/O-канал, и его стоит использовать только для отладки, а не для высокопроизводительного вывода.
<<Дополнение: роль кодировки и шрифта
Коллега выше верно указал на перенос строк, но есть ещё один фактор — ширина глифа в моноширинном шрифте.
В большинстве терминалов используется моноширинный шрифт (например, Consolas, Courier New). Однако символ '#' обычно занимает ровно одну позицию по ширине, а 'B' — тоже одну. Но есть нюанс:
- В некоторых шрифтах (особенно в старых растровых) символ 'B' имеет более сложную форму, требующую больше операций для рендеринга. Разница микроскопическая, но при тысячах итераций она накапливается.
- В Windows cmd.exe по умолчанию используется шрифт, который может переключаться между растровым и TrueType. При достижении правого края терминал может перерисовывать весь буфер, что для 'B' (как более "тяжёлого" символа) занимает больше времени.
Чтобы проверить, попробуйте сменить шрифт терминала на "Lucida Console" или "Consolas" — разница может уменьшиться. Или используйте Linux-терминал (например, gnome-terminal) — там таких проблем обычно нет, так как алгоритмы переноса строк оптимизированы лучше.
Также рекомендую измерять время без учёта первого прогрева JVM (JIT-компиляция). Запустите цикл 2-3 раза перед измерением, чтобы исключить влияние компиляции:
// Прогрев
for (int i = 0; i < 1000; i++) System.out.print("#");
System.out.print("\n");
// Реальный замер
long start = System.nanoTime();
for (int i = 0; i < 1000; i++) System.out.print("B");
long end = System.nanoTime();
System.out.println("Time: " + (end - start) / 1_000_000 + " ms");
Но даже с прогревом разница останется, если не убрать перенос строк. Так что главный совет — избегайте вывода в консоль в циклах для production-кода. Используйте логирование (Log4j, SLF4J) с асинхронными аппендерами.
Ответов пока нет.
Войдите чтобы оставить ответ
Блоги