Чи неефективно об'єднувати рядки один за одним?


11

Згадую з моїх днів програмування на C, що коли два струни об'єднані, ОС повинна виділити пам'ять для об'єднаного рядка, тоді програма може скопіювати весь текст рядка в нову область в пам'яті, тоді стара пам'ять повинна вручну бути звільненим. Тож якщо це робиться кілька разів, як у випадку приєднання до списку, ОС повинна постійно виділяти все більше і більше пам’яті, лише щоб випустити її після наступного конкатенації. Набагато кращим способом зробити це в C було б визначити загальний розмір об'єднаних рядків і виділити необхідну пам'ять для всього об'єднаного списку рядків.

Зараз у сучасних мовах програмування (наприклад, C #) я бачу, як вміст колекцій поєднується між собою, повторюючи колекцію та додаючи по одному рядки до однієї посилання на рядок. Невже це неефективно навіть із сучасними обчислювальними можливостями?


залиште це компілятору і профайлеру, вони будуть дбати про це, ваш час набагато дорожчий, ніж час на з'єднання рядків.
OZ_

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

Зауважте, що такі речі, як об'єднання рядків, як правило, обробляються бібліотечною функцією, а не операційною системою. ОС може залучатися до розподілу пам'яті, але, мабуть, не для відносно невеликих об'єктів, таких як рядки.
Калеб

@Caleb ОС бере участь у ВСІМ розподілі пам'яті. Якщо не дотримуватися цього правила, це тип витоку пам'яті. Виняток - коли у програмі є чітко зашифровані рядки; вони записуються у вигляді двійкових даних у створеній збірці. Але як тільки ви маніпулюєте (або, можливо, навіть призначили) рядок, її потрібно зберігати в пам'яті (тобто пам'ять повинна бути виділена).
JSideris

4
@Bizorke У типовому сценарії розподільник пам’яті на зразок malloc () (який є частиною стандартної бібліотеки C, а не ОС) використовується для виділення різних фрагментів пам’яті з пам’яті, яка вже була виділена ОС процесом. В ОС не потрібно втягуватися, якщо процес не вистачає пам'яті і не потрібно запитувати більше. Він також може брати участь на нижчому рівні, якщо виділення спричинить помилку на сторінці. Так, так, ОС в кінцевому підсумку забезпечує пам'ять, але це не обов'язково бере участь у частковому розподілі рядків та інших об'єктів всередині процесу.
Калеб

Відповіді:


21

Ваше пояснення, чому воно неефективне, є точним, принаймні мовою, з якою я знайомий (C, Java, C #), хоча я не погоджуюся з тим, що загальновизнаним є велика кількість конкатенації рядків. У коді C #, над яким я працюю, є багато використання StringBuilder, String.Formatі т. Д., Які є всіма методами збереження пам'яті, щоб уникнути перерозподілу.

Отже, щоб дійти до відповіді на ваше запитання, ми мусимо задати ще одне запитання: якщо ніколи насправді не проблема поєднувати рядки, то чому б класам подобалось StringBuilderі StringBufferіснувало ? Чому використання таких класів включено навіть у книги та програми програмування для початківців? Чому, здавалося б, передзрілі поради щодо оптимізації будуть настільки визначними?

Якби більшість розробників, що об'єднують рядки, базувались на своїй відповіді виключно на досвіді, більшість сказала б, що це ніколи не має значення, і використовуватиме такі інструменти на користь "читабельніших" for (int i=0; i<1000; i++) { strA += strB; }. Але вони його ніколи не вимірювали.

Справжню відповідь на це запитання можна знайти в цій відповіді "SO" , яка виявляє, що в одному випадку, при об'єднанні 50 000 рядків (що може залежати від вашої програми, навіть звичайні випадки), це призвело до 1000-кратного успіху .

Якщо продуктивність в буквальному сенсі нічого не означає, все одно об'єднуйтесь. Але я не погоджуюся з тим, що використовувати альтернативи (StringBuilder) складно або менш читабельно , і тому було б розумною практикою програмування, яка не повинна викликати захист "передчасної оптимізації".

ОНОВЛЕННЯ:

Я думаю, що це зводиться до цього, це знати свою платформу та дотримуватися її кращих практик, які, на жаль, не є універсальними . Два приклади з двох різних "сучасних мов":

  1. В іншому SO відповідь , то точна протилежність були знайдені характеристики (Array.join проти + =) , щоб бути іноді вірно в JavaScript . У деяких браузерах схоже, що конкатенація рядків оптимізується автоматично, а в інших випадках - не. Тож рекомендація (принаймні в цьому питанні) полягає в тому, щоб просто об'єднатись і не турбуватися про це.
  2. В іншому випадку компілятор Java може автоматично замінити конкатенацію на більш ефективну конструкцію, таку як StringBuilder. Однак, як вказували інші, це неефективізовано, не гарантовано, а використання StringBuilder не зашкодить читабельності. У цьому конкретному випадку я схильний рекомендувати не використовувати конкатенацію для великих колекцій або покладатися на недетерміновану поведінку компілятора Java. Аналогічно, в .NET ніколи не проводиться оптимізація сортування .

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


-1: містить основні BS. strA + strBце точно так же , як з допомогою StringBuilder. У ній є хіт для продуктивності 1x. Або 0x, залежно від способу вимірювання. Більш детально, codinghorror.com/blog/2009/01/…
amara

5
@sparkleshy: Я гадаю, що ТАК у відповіді використовує Java, а пов’язана стаття використовує C #. Я погоджуюся з тими, хто каже, що "залежить від впровадження" та "вимірюйте це для вашого конкретного середовища".
Кай Чан

1
@KaiChan: конкатенація струн в основному однакова в java і c #
amara

3
@sparkleshy - Точка прийнята, але використання StringBuilder, String.Join тощо для об'єднання рівно двох рядків рідко є рекомендацією. Далі, питання ОП конкретно стосується "вмісту колекцій, що об'єднуються разом", що не є випадком (де StringBuilder тощо є дуже застосовним). Незважаючи на те, я оновлю свій приклад, щоб бути більш зрозумілим.
Кевін Маккормік

3
Мене не хвилює мова для цього питання. Використання стрингбудівщиків за лаштунками в деяких мовах пояснює, чому може бути неефективним об'єднати цілий список рядків, що відповідає на моє запитання. Однак у цій відповіді було пояснено, що приєднання до списку потенційно може бути небезпечним, і рекомендував строкобудівник як альтернативу. Я рекомендую додати у відповідь використання компілятора за допомогою лайфхакерів за кадром, щоб уникнути можливої ​​втрати репутації чи неправильного тлумачення.
JSideris

2

Це неефективно, приблизно з описаних вами причин. Рядки в C # і Java незмінні. Операції над рядками повертають окремий екземпляр замість зміни оригіналу, на відміну від C. При об'єднанні декількох рядків на кожному кроці створюється окремий екземпляр. Виділення та згодом сміття, що збирає ці невикористані екземпляри, може спричинити показник ефективності. Тільки цього часу управління пам’яттю обробляє для вас сміттєзбірник.

І C #, і Java представляють клас StringBuilder як змінну рядок спеціально для цього типу завдань. Еквівалентом в C було б використання зв'язаного списку об'єднаних рядків замість того, щоб приєднувати їх до масиву. Також C # пропонує зручний метод приєднання на рядках для приєднання до колекції рядків.


1

Строго кажучи, це менш ефективне використання циклів процесора, тому ви правильно. Але що стосується часу розробника, витрат на обслуговування тощо. Якщо додати до рівняння витрати часу, майже завжди ефективніше робити те, що найпростіше, тоді, якщо потрібно, профіліруйте та оптимізуйте повільні біти.
"Перше правило оптимізації програми: не робіть цього. Друге правило оптимізації програми (лише для експертів!): Не робіть цього ще."


3
не дуже дієві правила, я думаю.
OZ_

@OZ_: Це широко використовувана цитата (Майкл А. Джексон) та ін., Подібний Дональду Кнуту ... Тоді є такий, який я зазвичай утримуюсь від використання "Більше обчислювальних гріхів робиться в ім'я ефективності ( не обов'язково цього досягати), ніж з будь-якої іншої причини - включаючи сліпу дурість ".
mattnz

2
Я мушу зазначити, що Майкл А. Джексон був британець, тому це оптимізація, а не оптимізація . У якийсь момент я дійсно повинен виправити сторінку вікіпедії . * 8 ')
Марк Бут

Я повністю згоден, ви повинні виправити ці орфографічні помилки. Хоча моєю рідною мовою є англійська англійська королева, мені легше розмовляти американською мовою в Інтернеті .......
mattnz

ніхто не подумає про користувачів. Ви можете зробити це дещо швидше для розробника для створення, але тоді кожен ваш клієнт страждає за це. Напишіть свій код для них, а не для вас.
gbjbaanb

1

Дуже важко нічого сказати про продуктивність без практичного тесту. Нещодавно я був дуже здивований, дізнавшись, що в JavaScript наївне з'єднання рядків зазвичай швидше, ніж рекомендоване рішення "скласти список і приєднатися" (тест тут , порівняйте t1 з t4). Я все ще спантеличений питанням, чому це відбувається.

Кілька питань, які ви можете задати, коли міркуєте про продуктивність (особливо щодо використання пам'яті): 1) наскільки великий мій внесок? 2) наскільки розумний мій компілятор? 3) як мій час роботи управляє пам'яттю? Це не є вичерпним, але це вихідний пункт.

  1. Наскільки великий мій внесок?

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

  2. Наскільки розумний мій компілятор?

    Багато компіляторів досить розумні, щоб "оптимізувати" змінні, які записуються, але ніколи не читаються. Так само, хороший компілятор може також змогти перетворити наївне з'єднання рядків у (основне) використання бібліотеки, і якщо багато з них зроблені без будь-якого зчитування, немає необхідності перетворювати його на рядок між цими операціями (навіть якщо ваш вихідний код, здається, робить саме це). Я не можу сказати, чи роблять якісь там компілятори чи ні в якій мірі це робиться (AFAIK Java принаймні замінює кілька кондатів в одному виразі послідовності операцій StringBuffer), але це можливість.

  3. Як мій час роботи керує пам'яттю?

    У сучасних процесорах вузьким місцем зазвичай є не процесор, а кеш; якщо ваш код отримує доступ до багатьох "віддалених" адрес пам'яті за короткий час, час, необхідний для переміщення всієї цієї пам'яті між рівнями кешу, переважає більшість оптимізацій в інструкціях, що використовуються. Це має особливе значення під час виконання з генераторами сміттєзбірників, оскільки останні створені змінні (наприклад, у межах тієї ж області функцій), як правило, знаходяться в суміжних адресах пам'яті. Ці періоди виконання також регулярно переміщують пам'ять між викликами методів.

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

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


Так, я погоджуюся, що це, безумовно, область, де компілятор теоретично може усвідомити, що ви намагаєтесь додати купу струн разом, а потім оптимізувати, як ніби ви використовуєте конструктор струн. Однак це навряд чи є дрібницею, і я не думаю, що це реалізовано в будь-яких сучасних компіляторах. Ти щойно дав мені чудову ідею для студентського дослідницького проекту: D.
JSideris

Перевірте цю відповідь , компілятор Java вже використовує StringBuilderпід кришкою, все, що потрібно було б зробити, це не викликати, toStringпоки змінна фактично не потрібна. Якщо я пригадую правильно, це робить це для одного виразу, я мою єдине сумнів у тому, застосовується чи ні для кількох висловлювань одним і тим же методом. Я нічого не знаю про внутрішні .NET, але я вважаю, що подібну стратегію може використовувати і компілятор C #.
mgibsonbr

0

Джоел ще раз написав чудову статтю на цю тему. Як зазначали деякі інші, вона сильно залежить від мови. Через те, як рядки реалізовані в C (нульове завершення, без поля довжини), звичайна програма бібліотеки strcat дуже неефективна. Джоел представляє альтернативу лише незначною зміною, яка набагато ефективніша.


-1

Чи неефективно об'єднувати рядки один за одним?

Немає.

Ви читали "Сумну трагедію театру мікрооптимізації" ?


4
"Передчасна оптимізація - корінь усього зла". - Кнут
Скотт Вілсон

4
Корінь всього зла в оптимізації сприймає цю фразу без контексту.
OZ_

Просто сказати, що щось правда, не наводячи жодних причин для підтримки, не корисно на такому форумі.
Едвард Странд

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