Почему в Java печать "B" медленнее, чем "#"? Разбор с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему "B" тормозит, а "#" летает? Разгадка культового вопроса Java

Если вы когда-нибудь писали простые циклы на Java для вывода символов в консоль, то, возможно, замечали странность: вывод строчной буквы 'B' может оказаться заметно медленнее, чем вывод решётки '#'. Этот эффект однажды взорвал Stack Overflow — вопрос набрал тысячи просмотров и голосов. Сегодня мы разберёмся, что стоит за этим феноменом, и как это связано с внутренними механизмами Java, консольного ввода-вывода и буферизации.

Постановка эксперимента

Представьте, что мы запускаем два цикла, которые печатают по 100 000 символов в консоль: один цикл выводит '#', другой — 'B'. На первый взгляд, операция идентична. Однако на многих системах (особенно Windows) первый цикл завершается за ~100 мс, а второй — за несколько секунд. Почему?

Давайте воспроизведём это на Java.

Код для теста

public class SlowPrintTest {
    public static void main(String[] args) {
        long start = System.nanoTime();
        for (int i = 0; i < 100_000; i++) {
            System.out.print("#");
        }
        long end = System.nanoTime();
        System.out.println("\nВремя для '#': " + (end - start) / 1_000_000 + " ms");

        start = System.nanoTime();
        for (int i = 0; i < 100_000; i++) {
            System.out.print("B");
        }
        end = System.nanoTime();
        System.out.println("\nВремя для 'B': " + (end - start) / 1_000_000 + " ms");
    }
}

Запустите этот код в терминале, а не в IDE (например, через java SlowPrintTest). Результат вас удивит. На Windows с CMD разница может быть в 10–50 раз.

Почему так происходит? Корень зла — в рендеринге шрифтов

Дело не в самом System.out.print() и не в JVM. Основная причина — в том, как операционная система и терминал обрабатывают вывод символов. System.out в Java — это обёртка над стандартным потоком вывода (stdout). Данные передаются в консоль, но скорость вывода на экран зависит от:

  • Ширины символа — многие старые консоли (например, CMD в Windows) используют моноширинные шрифты, но символ 'B' может требовать более сложного рендеринга, чем '#'.
  • Обновления экрана — консоль может выполнять полную перерисовку строки при выводе каждого символа, а не просто добавлять его.
  • Буферизации терминала — некоторые терминалы буферизируют вывод блоками, но если символ требует вычисления ширины глифа, это замедляет работу.

Наиболее ярко эффект проявляется в Windows Command Prompt (CMD) с растровыми шрифтами. Когда вы печатаете 'B', консоль пытается отобразить символ в графическом режиме, а для '#' — использует упрощённый подход. В современных терминалах (Windows Terminal, iTerm2, Alacritty) разница может быть менее заметна или отсутствовать.

Роль буферизации в Java

Важный аспект оптимизации вывода — использование буферизированных потоков. System.out по умолчанию не буферизирован (точнее, буферизирован на уровне JVM, но с маленьким размером). Каждый вызов print() может вызывать системный вызов write(), что очень дорого. Если мы добавим буферизацию через BufferedWriter, производительность вырастет, но разница между символами останется — она на стороне терминала.

// Пример с буферизацией
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
for (int i = 0; i < 100_000; i++) {
    bw.write("#");
}
bw.flush();

Даже с буферизацией, если терминал медленно отрисовывает 'B', общее время увеличится. Однако разница станет менее драматичной.

Практический эксперимент: меняем шрифт консоли

Попробуйте сменить шрифт в CMD на Consolas или Lucida Console. Разница между 'B' и '#' может уменьшиться или исчезнуть. Это прямое доказательство, что проблема в рендеринге.

Как обойти проблему? Советы для разработчиков

Если вы пишете производительный консольный вывод на Java, следуйте этим рекомендациям:

  • Используйте BufferedWriter — это снизит частоту системных вызовов.
  • Минимизируйте количество вызовов print() — формируйте строку через StringBuilder и выводите её одной порцией.
  • Выбирайте правильный терминал — для Windows используйте Windows Terminal вместо классического CMD.
  • Не забывайте про flush() — если вывод критичен по времени, контролируйте момент сброса буфера.
// Оптимизированный вариант
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100_000; i++) {
    sb.append('B');
}
System.out.print(sb.toString());

А что насчёт других языков?

Интересно, что подобный эффект наблюдался и в C# (в старой консоли Windows), и в C++ (с std::cout). Однако в Python разница менее выражена из-за более высокоуровневой обработки вывода. Но если вы пишете на Java и столкнулись с загадочным замедлением — теперь вы знаете, в чём дело.

Вывод: что запомнить?

Феномен «B медленнее #» — это не баг JVM и не ошибка в коде. Это особенность взаимодействия стандартного потока вывода с конкретной реализацией консоли. Основные выводы:

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

Теперь, когда вы столкнётесь с похожей задачей, вы сможете не просто удивляться, а диагностировать проблему и применить правильные инструменты оптимизации. Удачи в экспериментах!

Блоги

Рекомендаций по книгам