Наскільки погано викликати println () часто, ніж об'єднувати рядки разом і викликати його один раз?


23

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

Наприклад, наскільки менш ефективною вона є

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

vs.

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 ++, оскільки кожен раз, коли дані записуються у вихідний потік, повинен здійснюватися системний виклик і процес повинен переходити в режим ядра (що дуже дорого)?


Хоча це дуже мало коду, я мушу сказати, що мені було цікаво те саме. Було б добре визначити відповідь на це раз і назавжди
Саймон Форсберг

@ SimonAndréForsberg Я не впевнений, чи застосовний він до Java, оскільки він працює на віртуальній машині, але на мовах нижчого рівня, таких як C / C ++, я думаю, що це буде дорого, оскільки кожен раз, коли щось записує у вихідний потік, системний виклик повинні бути зроблені.

Там ця розглянути також: stackoverflow.com/questions/21947452 / ...
ХІК

1
Я мушу сказати, що в цьому я не бачу сенсу. Під час взаємодії з користувачем через термінал, я не можу уявити жодних проблем із продуктивністю, тому що зазвичай друкується не так багато. І програми з GUI або webapp повинні записувати в файл журналу (зазвичай, використовуючи фреймворк).
Енді

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-константа відбуватиметься під час компіляції і не матиме ніякого впливу на продуктивність. Рядки читаються, і ви можете просто рухатися далі.

Тепер про:

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

vs.

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

Другий варіант значно швидший. Я підкажу про 2X як швидко .... чому?

Оскільки 90% роботи (з широкою помилкою) роботи не пов'язане із скиданням символів на вихід, а є накладними, необхідними для забезпечення виводу для запису на нього.

Синхронізація

System.outє PrintStream. Усі реабілітації Java, які мені відомі, внутрішньо синхронізують PrintStream: Дивіться код на GrepCode! .

Що це означає для вашого коду?

Це означає, що кожного разу, коли ви телефонуєте, System.out.println(...)ви синхронізуєте свою модель пам'яті, ви перевіряєте і чекаєте блокування. Будь-які інші потоки, що викликають System.out, також будуть заблоковані.

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

Промивання

Кожен 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

Вихід (помилково) виглядає так:

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

Ви також повинні врахувати проблему, яка "\n"може бути не правильним термінатором. printlnавтоматично припиняє рядок з потрібними символами (символами), але вкладення a \nу рядок безпосередньо може спричинити проблеми. Якщо ви хочете зробити це правильно, можливо, вам доведеться скористатися форматуванням рядків або line.separatorвластивістю системи . printlnнабагато чистіше.
user2357112 підтримує Monica

3
Це все чудовий аналіз, тому +1 точно, але я б стверджував, що після того, як ви прагнете консолідувати вихід, ці незначні відмінності в продуктивності вилітають у вікно. Якщо алгоритм вашої програми працює швидше, ніж виведення результатів (при цьому невеликому рівні виведення), ви можете друкувати кожен символ один за одним і не помічати різниці.
Девід Харкнесс

Я вважаю, що це різниця між Java та C / C ++, що вихід синхронізований. Я говорю це тому, що я пам'ятаю написання багатопотокової програми та виникли проблеми зі скасованим виведенням, якщо різні потоки намагаються записати, щоб записати на консоль. Хтось може це перевірити?

6
Важливо також пам’ятати, що жодна з цих швидкостей взагалі не має значення, коли ставиться поруч із функцією, яка чекає на введення користувача.
vmrob

2

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

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

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


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).

iostreams також мають sync_with_stdioвластивість, яка визначає, синхронізується вихід з iostreams із введенням у стилі C (наприклад, getchar). За замовчуванням sync_with_stdioвстановлено значення true, тому, якщо, наприклад, ви пишете std::cout, а потім читаєте через getchar, дані, до яких ви писали, coutбудуть видалені, коли getcharвикликаються. Ви можете встановити sync_with_stdioзначення false, щоб відключити це (зазвичай це робиться для покращення продуктивності).

sync_with_stdioтакож контролює ступінь синхронізації між потоками. Якщо синхронізація увімкнена (за замовчуванням) запис у iostream з декількох потоків може призвести до перемежування даних з потоків, але запобігає будь-яким перегоновим умовам. IOW, ваша програма запустить і дасть вихід, але якщо більше потоку записує в потік одночасно, довільне змішування даних з різних потоків, як правило, робить висновок досить марним.

Якщо ви повертаєте від синхронізації, а потім синхронізувати доступ з декількох потоків стає повністю ваша відповідальність , як добре. Одночасне записування з декількох потоків може / призведе до перегону даних, а це означає, що код має не визначену поведінку.

Підсумок

С ++ за замовчуванням намагається збалансувати швидкість із безпекою. Результат є досить успішним для однопотокового коду, але менш для багатопотокового коду. Багатопотоковий код, як правило, повинен гарантувати, що тільки один потік записує в потік одночасно, щоб отримати корисний вихід.


1. Можна вимкнути буферизацію для потоку, але насправді це досить незвично, і коли / якщо хтось це робить, це, мабуть, з досить конкретної причини, наприклад, щоб забезпечити негайне захоплення всього виходу, незважаючи на вплив на продуктивність . У будь-якому випадку це відбувається лише в тому випадку, якщо код робить це явно.


13
" У мовах вищого рівня, таких як C і C ++, це менше проблеми, ніж у Java. " - що? C і C ++ - це мови нижчого рівня, ніж Java. Крім того, ви забули свої лінійні термінатори.
user2357112 підтримує Monica

1
На всьому протязі я вказую на об'єктивну основу для Java як мови нижчого рівня. Не впевнені, про які термінатори ви говорите.
Джері Коффін

2
Java також з'єднує час компіляції. Наприклад, "2^31 - 1 = " + Integer.MAX_VALUEзберігається як одна інтернована рядок (JLS 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текст у команді консолі, було б набагато краще мати його як окремий ресурс і прочитати його та записати на екран на запит.

Але зазвичай це суміш динамічної та статичної частин. Скажімо, у нас з одного боку є деякі головні дані замовлення, а з іншого - нерухомі статичні текстові частини, і ці речі потрібно змішати, щоб сформувати аркуш підтвердження замовлення. Знову ж, і в цьому випадку краще мати окремий текстовий файл ресурсу: Ресурс був би шаблоном, що містить певну кількість символів (заповнювачів), які під час виконання замінюються фактичними даними замовлення.

Відмежування мови програмування від природної мови має багато переваг - серед них є інтернаціоналізація: Вам, можливо, доведеться перекласти текст, якщо ви хочете стати багатомовним зі своїм програмним забезпеченням. Крім того, навіщо потрібен крок компіляції, якщо ви хочете лише виправити текст, скажіть, виправте деякі неправильні написання.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.