Чому струни такі повільні?


23

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


11
Якщо ви знаєте, що ці «середні операції» є міфічними, чи можете ви принаймні сказати нам, що таке деякі з них? Зважаючи на те, що ви задаєте таке невиразне запитання, важко довіритися вашому твердженню, що ці не визначені операції справді є міфічними.
seh

1
@seh, на жаль, я насправді на це не можу відповісти. Кілька разів я насправді запитував людей, які струни повільніші, вони просто видають плечима і кажуть "вони просто повільні". Крім того, якби я мав більш конкретну інформацію, це було б питанням щодо СО, а не Програмістів; це вже якось прикордонне.
Попс

У чому сенс? Якщо сказані рядки насправді повільні, ви перестанете їх використовувати?
Тулен Кордова

Забудь це. Якщо хтось скаже вам таку нісенітницю, відповідь на запитання: "Дійсно? Це вони? Чи слід тоді використовувати int-масив?"
Інго

Відповіді:


47

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

Наприклад, додавання двох чисел зазвичай займає 2-4 інструкції ASM. Об'єднання ("додавання") двох рядків вимагає нового розподілу пам'яті та однієї або двох струнних копій, включаючи всю нитку.

Певні мовні фактори можуть погіршити ситуацію. Наприклад, у C, рядок - це просто вказівник на масив символів, що закінчується нулем. Це означає, що ви не знаєте, скільки триває, тому немає можливості оптимізувати цикл копіювання рядків за допомогою операцій швидкого переміщення; Вам потрібно скопіювати один символ одночасно, щоб ви могли протестувати кожен байт для нульового термінатора.


4
І деякі мови роблять це набагато краще: кодування Delphi довжини рядка на початку масиву робить конкатенацію рядків дуже швидкою.
Френк Шірар

4
@gablin: Це також допомагає, роблячи копіювання рядків набагато швидше. Коли ви знаєте передній розмір, вам не доведеться копіювати один байт за один раз і перевіряти кожен байт на нульовий термінатор, тому ви можете використовувати повний розмір будь-якого реєстру, включаючи SIMD, для руху даних, роблячи це в 16 разів швидше.
Мейсон Уілер

4
@mathepic: Так, і це добре, наскільки це займе у вас, але коли ви почнете взаємодія з libc або іншим зовнішнім кодом, він очікує char*, що не strbuf, а ви повернетесь до площі 1. Є тільки стільки ви може зробити, коли погана конструкція випечена на мові.
Мейсон Уілер

6
@mathepic: Звичайно, bufвказівник є. Я ніколи не мав на увазі, що це недоступно; скоріше, що це потрібно. Будь-який код, який не знає про ваш оптимізований, але нестандартний тип рядка, включаючи речі, настільки фундаментальні, як стандартна бібліотека , все ще повинен відкидатися на повільному, небезпечному char*. Ви можете викликати цей FUD, якщо хочете, але це не робить його неправдою.
Мейсон Уілер

7
Люди, є колонка Джоеля Спольського про точку Френка Ширера: Повернутися до основ
user16764

14

Це стара нитка, і я думаю, що інші відповіді чудові, але щось не помічаю, тож ось мої (пізні) 2 копійки.

Синтаксичне покриття цукру приховує складність

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

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

Деталі впровадження

Гарний масив Ol '

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

Це має сенс, майте на увазі, оскільки ви хочете, щоб доступ до рядка в цілому був швидким. Але це означає потенційно жахливі витрати, коли ви хочете маніпулювати цим рядком. Доступ до елемента в середині може бути швидким, якщо ви знаєте, за яким індексом ви шукаєте , але шукати елемент на основі умови - це не так.

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

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

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

Підтримка Unicode

Крім того, і знову це пов'язано з тим, що синтаксичне цукрове покриття вашої мови приховує це від вас, щоб грати добре, ви часто не вважаєте це умовами підтримки unicode (особливо до тих пір, поки вам це не потрібно. і вдарив ту стіну). І деякі мови, будучи передумовою, не реалізують рядки з базовими масивами простих 8-бітових примітивних символів. Вони випікаються в UTF-8 або UTF-16 або що-у вас є підтримка для вас, і наслідком цього є надзвичайно більший обсяг пам'яті, який часто не потрібен, і більший час на обробку пам'яті, обробка струн, і реалізувати всю логіку, яка йде рука об руку, маніпулюючи кодовими точками.


Результати всього цього полягають у тому, що коли ви робите щось еквівалентне в псевдокоді:

hello = "hello,"
world = " world!"
str = hello + world

Це може бути, незважаючи на всі зусилля, які доклали розробники мови, щоб вони поводилися так, як ви хотіли, - простим як:

a = 1;
b = 2;
shouldBeThree = a + b

У подальшому ви можете прочитати:


Гарне доповнення до нинішньої дискусії.
Авель

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

@Abel: спасибі, мені здалося, було місце для більш загальних деталей.
haylem

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

1

Фраза "середня операція", ймовірно, скорочена для однієї операції теоретичної машини з збереженою програмою з випадковим доступом . Це теоретична машина, яку прийнято використовувати для аналізу часу виконання різних алгоритмів.

Узагальнені операції зазвичай приймаються як завантаження, додавання, віднімання, зберігання, розгалуження. Можливо також читати, друкувати та зупиняти.

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


1

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

Це також залежить від того, що ви маєте на увазі під «повільним». Більшість програм буде обробляти рядки досить швидко для того, що потрібно. Порівняння рядків може бути не настільки швидким, як порівняння двох входів, але лише профілювання виявить, що "повільно" означає для вашої програми.


0

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


2
Це не обов'язково.
user16764

3
Supercalifragilisticexpialidocious
Спойк

s / слово / склад / g
Калеб

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