Насколько плохо вызывать println () чаще, чем объединять строки и вызывать их один раз?

23

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

Например, насколько менее эффективно иметь

System.out.println("Good morning.");
System.out.println("Please enter your name");

против

System.out.println("Good morning.\nPlease enter your name");

В примере разница только в одном вызове, println()но что, если это больше?

Относительно примечания, заявления, связанные с печатью текста, могут выглядеть странно при просмотре исходного кода, если текст для печати длинный. Предполагая, что сам текст не может быть сокращен, что можно сделать? Должен ли это быть случай, когда println()будет сделано несколько звонков? Кто-то однажды сказал мне, что строка кода не должна превышать 80 символов (IIRC), так что бы вы сделали с

System.out.println("Good morning everyone. I am here today to present you with a very, very lengthy sentence in order to prove a point about how it looks strange amongst other code.");

То же самое верно для языков, таких как C / C ++, поскольку каждый раз, когда данные записываются в выходной поток, должен выполняться системный вызов, и процесс должен переходить в режим ядра (который очень дорогой)?

Celeritas
источник
Хотя это очень маленький код, я должен сказать, что мне было интересно то же самое. Было бы неплохо определиться с ответом на этот раз и навсегда
Саймон Форсберг
@ SimonAndréForsberg Я не уверен, применимо ли это к Java, потому что оно работает на виртуальной машине, но на языках более низкого уровня, таких как C / C ++, я бы подумал, что это будет дорого, так как каждый раз, когда что-то записывает в выходной поток, системный вызов необходимо сделать.
И это тоже нужно учитывать: stackoverflow.com/questions/21947452/…
hjk
1
Я должен сказать, что я не вижу смысла здесь. При взаимодействии с пользователем через терминал я не могу представить никаких проблем с производительностью, потому что обычно там не так много печатать. А приложения с графическим интерфейсом или веб-приложением должны записывать в лог-файл (обычно с использованием фреймворка).
Энди
1
Если вы говорите доброе утро, вы делаете это один или два раза в день. Оптимизация не является проблемой. Если это что-то еще, вы должны профиль, чтобы знать, если это проблема. Код, над которым я работаю, замедляет работу кода до тех пор, пока вы не создадите многострочный буфер и не создадите дамп текста за один вызов.
Mattnz

Ответы:

29

Здесь есть две «силы» в напряжении: производительность против читабельности.

Давайте сначала рассмотрим третью проблему - длинные строки:

System.out.println("Good morning everyone. I am here today to present you with a very, very lengthy sentence in order to prove a point about how it looks strange amongst other code.");

Лучший способ реализовать это и сохранить читабельность - использовать конкатенацию строк:

System.out.println("Good morning everyone. I am here today to present you "
                 + "with a very, very lengthy sentence in order to prove a "
                 + "point about how it looks strange amongst other code.");

Конкатенация String-constant происходит во время компиляции и никак не влияет на производительность. Строки читабельны, и вы можете просто двигаться дальше.

Теперь о:

System.out.println("Good morning.");
System.out.println("Please enter your name");

против

System.out.println("Good morning.\nPlease enter your name");

Второй вариант значительно быстрее. Я посоветую примерно в 2 раза быстрее .... почему?

Потому что 90% (с большим пределом погрешности) работы не связаны с выгрузкой символов в вывод, а являются накладными расходами, необходимыми для защиты вывода для записи в него.

синхронизация

System.outявляется PrintStream. Все известные мне Java-реализации внутренне синхронизируют PrintStream: см. Код на GrepCode! ,

Что это значит для вашего кода?

Это означает, что каждый раз, когда вы звоните, System.out.println(...)вы синхронизируете модель памяти, вы проверяете и ожидаете блокировки. Любые другие потоки, вызывающие System.out, также будут заблокированы.

В однопоточных приложениях влияние System.out.println()часто ограничивается производительностью ввода-вывода вашей системы, как быстро вы можете записывать в файл. В многопоточных приложениях блокировка может быть более серьезной проблемой, чем IO.

смывание

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

Некоторые цифры

Я собрал следующий маленький тест:

public class ConsolePerf {

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            benchmark("Warm " + i);
        }
        benchmark("real");
    }

    private static void benchmark(String string) {
        benchString(string + "short", "This is a short String");
        benchString(string + "long", "This is a long String with a number of newlines\n"
                  + "in it, that should simulate\n"
                  + "printing some long sentences and log\n"
                  + "messages.");

    }

    private static final int REPS = 1000;

    private static void benchString(String name, String value) {
        long time = System.nanoTime();
        for (int i = 0; i < REPS; i++) {
            System.out.println(value);
        }
        double ms = (System.nanoTime() - time) / 1000000.0;
        System.err.printf("%s run in%n    %12.3fms%n    %12.3f lines per ms%n    %12.3f chars per ms%n",
                name, ms, REPS/ms, REPS * (value.length() + 1) / ms);

    }


}

Код относительно прост, он многократно печатает короткую или длинную строку для вывода. Длинная строка содержит несколько новых строк. Он измеряет, сколько времени требуется для печати 1000 итераций каждой.

Если я запускаю его в Unix (Linux) в командной строке, и перенаправить STDOUTк /dev/null, и распечатать фактические результаты STDERR, я могу сделать следующее:

java -cp . ConsolePerf > /dev/null 2> ../errlog

Вывод (в errlog) выглядит так:

Warm 0short run in
           7.264ms
         137.667 lines per ms
        3166.345 chars per ms
Warm 0long run in
           1.661ms
         602.051 lines per ms
       74654.317 chars per ms
Warm 1short run in
           1.615ms
         619.327 lines per ms
       14244.511 chars per ms
Warm 1long run in
           2.524ms
         396.238 lines per ms
       49133.487 chars per ms
.......
Warm 99short run in
           1.159ms
         862.569 lines per ms
       19839.079 chars per ms
Warm 99long run in
           1.213ms
         824.393 lines per ms
      102224.706 chars per ms
realshort run in
           1.204ms
         830.520 lines per ms
       19101.959 chars per ms
reallong run in
           1.215ms
         823.160 lines per ms
      102071.811 chars per ms

Что это значит? Позвольте мне повторить последнюю строфу:

realshort run in
           1.204ms
         830.520 lines per ms
       19101.959 chars per ms
reallong run in
           1.215ms
         823.160 lines per ms
      102071.811 chars per ms

Это означает, что для всех намерений и целей, даже несмотря на то, что «длинная» строка примерно в 5 раз длиннее и содержит несколько новых строк, для вывода требуется примерно столько же времени, сколько для короткой строки.

Количество символов в секунду в долгосрочной перспективе в 5 раз больше, а истекшее время примерно одинаково .....

Другими словами, ваша производительность масштабируется относительно количества ваших отпечатков, а не того , что они печатают.

Обновление: что произойдет, если вы перенаправите в файл, а не в / dev / null?

realshort run in
           2.592ms
         385.815 lines per ms
        8873.755 chars per ms
reallong run in
           2.686ms
         372.306 lines per ms
       46165.955 chars per ms

Это намного медленнее, но пропорции примерно одинаковы ....

rolfl
источник
Добавлены некоторые номера производительности.
rolfl
Вы также должны рассмотреть проблему, которая "\n"не может быть правильным ограничителем строки. printlnавтоматически завершит строку правильным символом (ами), но вставка \nстроки в вашу строку может вызвать проблемы. Если вы хотите сделать это правильно, вам может потребоваться использовать форматирование строки или line.separatorсистемное свойство . printlnнамного чище
user2357112 поддерживает Monica
3
Это все отличный анализ, поэтому наверняка +1, но я бы сказал, что как только вы перейдете на консольный вывод, эти незначительные различия в производительности вылетят в окно. Если алгоритм вашей программы работает быстрее, чем вывод результатов (при этом небольшом уровне вывода), вы можете печатать каждый символ один за другим и не замечать разницы.
Дэвид Харкнесс
Я считаю, что это разница между Java и C / C ++ в том, что вывод синхронизирован. Я говорю это потому, что вспоминаю о написании многопоточной программы и проблемах с искаженным выводом, если разные потоки пытаются писать для записи в консоль. Кто-нибудь может это проверить?
6
Также важно помнить, что ни одна из этих скоростей вообще не имеет значения, если поместить их рядом с функцией, которая ожидает ввода пользователя.
vmrob
2

Я не думаю, что наличие множества printlns - это проблема дизайна вообще. Я вижу это в том, что это можно сделать с помощью статического анализатора кода, если это действительно проблема.

Но это не проблема, потому что большинство людей не делают IO, как это. Когда им действительно нужно сделать много операций ввода-вывода, они используют буферизованные (BufferedReader, BufferedWriter и т. Д.), Когда вход буферизируется, вы увидите, что производительность достаточно похожа, и вам не нужно беспокоиться о наличии куча printlnили мало println.

Итак, чтобы ответить на оригинальный вопрос. Я бы сказал, неплохо, если вы используете printlnдля распечатки несколько вещей, которые используют большинство людей println.

InformedA
источник
1

В языках более высокого уровня, таких как C и C ++, это меньше проблем, чем в Java.

Прежде всего, C и C ++ определяют конкатенацию строк во время компиляции, так что вы можете сделать что-то вроде:

std::cout << "Good morning everyone. I am here today to present you with a very, "
    "very lengthy sentence in order to prove a point about how it looks strange "
    "amongst other code.";

В таком случае объединение строк - это не просто оптимизация, которую вы можете в значительной степени, обычно (и т. Д.), Зависит от компилятора. Скорее, это прямо требуется стандартами C и C ++ (фаза 6 перевода: «Литеральные токены смежных строк объединяются»).

Хотя это происходит за счет небольшой дополнительной сложности компилятора и реализации, C и C ++ делают немного больше, чтобы скрыть сложность эффективного вывода результатов от программиста. Java гораздо больше похожа на ассемблер - каждый вызов System.out.printlnгораздо более непосредственно переводится в вызов основной операции для записи данных в консоль. Если вы хотите буферизовать для повышения эффективности, это должно быть предоставлено отдельно.

Это означает, например, что в C ++, переписывая предыдущий пример, что-то вроде этого:

std::cout << "Good morning everyone. I am here today to present you with a very, ";
std::cout << "very lengthy sentence in order to prove a point about how it looks ";       
std::cout << "strange amongst other code.";

... обычно не будет 1 практически не влияют на эффективность. Каждое использование coutпросто помещает данные в буфер. Этот буфер будет сброшен в нижележащий поток, когда буфер заполнится, или код попытается прочитать входные данные из использования (например, с помощью std::cin).

iostreamУ s также есть sync_with_stdioсвойство, которое определяет, синхронизирован ли вывод из iostreams с вводом в стиле C (например, getchar). По умолчанию sync_with_stdioустановлено значение true, поэтому, если, например, вы записываете std::cout, а затем читаете через getchar, записанные вами данные coutбудут сбрасываться при getcharвызове. Вы можете установить sync_with_stdioзначение false, чтобы отключить это (обычно это делается для повышения производительности).

sync_with_stdioтакже контролирует степень синхронизации между потоками. Если синхронизация включена (по умолчанию), запись в iostream из нескольких потоков может привести к тому, что данные из потоков будут чередоваться, но предотвратит любые условия гонки. Итак, ваша программа будет работать и производить вывод, но если одновременно в поток записывается более одного потока, произвольное перемешивание данных из разных потоков обычно делает вывод довольно бесполезным.

Если вы отключите синхронизацию, то ответственность за синхронизацию доступа из нескольких потоков также полностью ложится на вас. Одновременная запись из нескольких потоков может / приведет к гонке данных, что означает, что код имеет неопределенное поведение.

Резюме

По умолчанию C ++ пытается сбалансировать скорость с безопасностью. Результат довольно успешен для однопоточного кода, но менее для многопоточного кода. Многопоточный код обычно должен гарантировать, что только один поток записывает в поток за раз, чтобы произвести полезный вывод.


1. Можно отключить буферизацию для потока, но на самом деле это довольно необычно, и когда / если кто-то делает это, это, вероятно, по довольно конкретной причине, например, гарантируя, что весь вывод захватывается немедленно, несмотря на влияние на производительность. , В любом случае это происходит только в том случае, если код делает это явно.

Джерри Гроб
источник
13
« В языках более высокого уровня, таких как C и C ++, это меньше проблем, чем в Java. » - что? C и C ++ являются языками более низкого уровня, чем Java. Кроме того, вы забыли свои терминаторы строки.
user2357112 поддерживает Monica
1
Повсюду я указываю, что объективной основой для Java является язык более низкого уровня. Не уверен, о каких ограничителях вы говорите.
Джерри Гроб
2
Java также выполняет конкатенацию во время компиляции. Например, "2^31 - 1 = " + Integer.MAX_VALUEхранится в виде одной строки интернировано (ПСБ Sec 3.10.5 и 15.28 ).
200_успех
2
@ 200_success: Java, выполняющий конкатенацию строк во время компиляции, похоже, сводится к §15.18.1: «Объект String создается заново (§12.5), если выражение не является константным выражением времени компиляции (§15.28)». Кажется, это позволяет, но не требует, чтобы конкатенация выполнялась во время компиляции. Т.е., результат должен быть заново создан, если входные данные не являются константами времени компиляции, но не требуется никаких требований в обоих направлениях, если они являются константами времени компиляции. Чтобы требовать конкатенации во время компиляции, вы должны прочитать его (подразумеваемое) «если» как действительно означающее «тогда и только тогда».
Джерри Гроб
2
@Phoshi: Попробуйте с ресурсами, даже не очень похоже на RAII. RAII позволяет классу управлять ресурсами, но попытка использования ресурсов требует клиентского кода для управления ресурсами. Особенности (абстракции, точнее), которые есть у одного, а у другого отсутствуют, полностью актуальны - фактически именно они делают один язык более высоким уровнем, чем другой.
Джерри Гроб
1

Хотя производительность здесь не является проблемой, плохая читаемость множества printlnоператоров указывает на отсутствующий аспект дизайна.

Почему мы пишем последовательность из многих printlnутверждений? Если бы это был только один фиксированный текстовый блок, такой как --helpтекст в консольной команде, было бы намного лучше иметь его в качестве отдельного ресурса, прочитать его и записать на экран по запросу.

Но обычно это смесь динамических и статических частей. Допустим, у нас есть несколько простых данных заказа, с одной стороны, и несколько фиксированных статических текстовых частей, с другой стороны, и эти вещи нужно смешать вместе, чтобы сформировать лист подтверждения заказа. Опять же, и в этом случае лучше иметь отдельный текстовый файл ресурсов: ресурс будет шаблоном, содержащим какие-то символы (заполнители), которые во время выполнения заменяются фактическими данными заказа.

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

rplantiko
источник