Законність COW std :: виконання рядків в C ++ 11


117

Я розумів, що копіювання за записом не є життєздатним способом реалізації відповідного std::string в C ++ 11, але, коли нещодавно він з'явився в дискусії, я виявив, що не в змозі безпосередньо підтримати це твердження.

Чи я правда, що C ++ 11 не допускає реалізацію на основі COW std::string ?

Якщо так, то це обмеження прямо вказано десь у новому стандарті (де)?

Або це обмеження мається на увазі в тому сенсі, що саме сукупний вплив нових вимог на std::stringце перешкоджає впровадженню роботи на основі КОР std::string. У цьому випадку мене зацікавить виведення стилю глави та вірша "C ++ 11 фактично забороняє std::stringреалізацію на основі COW ".


5
Помилка GCC для їх рядка COW - gcc.gnu.org/bugzilla/show_bug.cgi?id=21334#c45 . Один з помилок, що відстежують нову реалізацію компілятора
user7610

Відповіді:


120

Це не дозволено, оскільки відповідно до стандарту 21.4.1 p6, інвалідність ітераторів / посилань дозволена

- як аргумент для будь-якої стандартної функції бібліотеки, яка приймає посилання на non-const basic_string як аргумент.

- виклик функцій, що не входять в const, крім оператора [], на, спереду, ззаду, почати, rbegin, закінчити і виконувати.

Для рядка COW для виклику non-const operator[]потрібно буде зробити копію (та недійсні посилання), що заборонено пунктом вище. Отже, більше не законно мати рядок COW в C ++ 11.


4
Деякі обґрунтування: N2534
ММ

8
-1 Логіка не тримає води. На момент копіювання COW немає жодних посилань чи ітераторів, які можуть бути визнані недійсними, вся суть у виконанні копіювання полягає в тому, що зараз отримуються такі посилання або ітератори, тому копіювання необхідно. Але все ж може бути, що C ++ 11 забороняє реалізацію COW.
Ура та хт. - Альф

11
@ Cheersandhth.-Alf: Логіку можна побачити в наступному, якщо дозволено std::string a("something"); char& c1 = a[0]; std::string b(a); char& c2 = a[1]; COW : c1 - посилання на a. Потім ви "копіюєте" a. Потім, коли ви намагаєтесь взяти посилання вдруге, він повинен зробити копію, щоб отримати неконст-посилання, оскільки є два рядки, які вказують на один і той же буфер. Це могло б визнати недійсним перше звернення, яке було зроблено, і суперечить вищевикладеному розділу.
Дейв S

9
@ Cheersandhth.-Альф, відповідно до цього , принаймні реалізація КРС GCC робить саме те, що говорить DaveS. Отож, принаймні, такий стиль COW заборонений стандартом.
Тавіан Барнс

4
@Alf: Ця відповідь стверджує, що non-const operator[](1) повинен зробити копію, і що (2) це робити незаконно. З яким із цих двох пунктів ви не згодні? Дивлячись на ваш перший коментар, здається, що реалізація може поділяти рядок, щонайменше, відповідно до цієї вимоги, до моменту доступу до неї, але для того, щоб і доступ для читання і запису повинен був скасувати його доступ. Це ваші міркування?
Бен Войгт

48

Відповіді на Dave S і gbjbaanb є правильними . (І права Люка Дантона теж вірні, хоча це скоріше побічний ефект заборони рядків COW, а не оригінальне правило, яке забороняє це.)

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

std::string s("str");
const char* p = s.data();
{
    std::string s2(s);
    (void) s[0];
}
std::cout << *p << '\n';  // p is dangling

Суть цього прикладу полягає в тому, щоб продемонструвати, чому посилальний рядок підрахунку GCC (COW) недійсний у C ++ 11. Стандарт C ++ 11 вимагає, щоб цей код працював правильно. Ніщо в коді не дозволяє pвизнати недійсним C ++ 11.

Використовуючи стару std::stringреалізовану посилання GCC , цей код має невизначене поведінку, оскільки p він недійсний, перетворюючись на звисаючий покажчик. (Що трапляється, це те, що коли s2він побудований, він обмінюється даними s, але для отримання посилань, що не мають const, через це s[0]потрібно, щоб дані не були спільними, тому s"копія при записі", тому що посилання s[0]потенційно може бути використане для запису s, потім s2переходить поза рамками, знищуючи масив, на який вказує p).

Стандарт C ++ 03 явно дозволяє цю поведінку в 21.3 [lib.basic.string] p5, де йдеться про те, що після виклику до data()першого дзвінка доoperator[]() може бути визнано недійсним покажчики, посилання та ітератори. Отже, рядок COW GCC був дійсною реалізацією C ++ 03.

Стандарт C ++ 11 більше не допускає такої поведінки, оскільки жоден виклик не operator[]()може визнати недійсним покажчики, посилання чи ітератори, незалежно від того, чи слідкують вони за викликом data().

Отже, наведений вище приклад повинен працювати в C ++ 11, але не працює з рядком COW libstdc ++, отже, такий тип рядка COW заборонений у C ++ 11.


3
Реалізація, яка не дає доступу до виклику .data()(та при кожному поверненні вказівника, посилання чи ітератора), не страждає від цієї проблеми. Тобто (інваріантний) буфер в будь-який час не є загальним або в іншому випадку надається без зовнішніх змін. Я подумав, що ви задумали коментар щодо цього прикладу як неофіційного звіту про помилку як коментар, дуже шкода, що його не зрозуміли! Але як ви бачите, розглядаючи таку реалізацію, як я описую тут, яка добре працює на C ++ 11, коли noexceptвимоги ігноруються, приклад не говорить про формальне. Я можу надати код, якщо хочете.
Ура та хт. - Альф

7
Якщо ви скасуєте доступ майже до кожного доступу до рядка, ви втратите всю користь спільного доступу. Реалізація COW повинна бути практичною для стандартної бібліотеки, яка б заважала використовувати це як std::string, і я щиро сумніваюся, що ви можете продемонструвати корисну, ефективну рядок COW, що відповідає вимогам недійсності C ++ 11. Тому я стверджую, що noexceptтехнічні характеристики, які були додані в останню хвилину, є наслідком заборони рядків COW, а не основної причини. N2668 видається абсолютно зрозумілим, чому ви продовжуєте заперечувати чіткі докази намірів комітету, викладені там?
Jonathan Wakely

Також пам’ятайте, що data()це функція члена const, тому повинно бути безпечно дзвонити одночасно з іншими членами const і, наприклад, дзвонити data()одночасно з іншим потоком, роблячи копію рядка. Таким чином, вам знадобляться всі накладні витрати на мютекс для кожної струнної операції, навіть const, або складність незміненої посилальної структури, що рахується без блокування, і зрештою, ви отримуєте спільний доступ лише якщо ви ніколи не змінюєте або отримуєте доступ ваші рядки, так багато, багато рядків матимуть посилання один. Будь ласка, надайте код, сміливо ігноруйте noexceptгарантії.
Jonathan Wakely

2
Просто зчепивши якийсь код, тепер я виявив, що існує 129 basic_stringчленів, плюс безкоштовні функції. Вартість абстракції: цей неоптимізований свіжий нульовий код версії на манжеті на 50 до 100% повільніше і для g ++, і для MSVC. Це не забезпечує безпеку потоків (достатньо простого використання shared_ptr, я думаю), і достатньо просто підтримати сортування словника з метою хронометражу, але помилки модулів це доводить те, що basic_stringдозволена кількість посилань дозволена, за винятком noexceptвимог C ++ . github.com/alfps/In-principle-demo-of-ref-count-basic_string
Ура та чт. - Альф


20

Це так, CoW - прийнятний механізм для створення швидших рядків ... але ...

це робить багатопотокове кодування повільнішим (все це блокування, щоб перевірити, чи ви єдиний, хто пише, що вбиває продуктивність при використанні багатьох рядків). Це було основною причиною того, що коров’яків було вбито років тому.

Інші причини полягають у тому, що []оператор поверне вам рядкові дані, без будь-якого захисту, щоб ви перезаписали рядок, хто інший розраховує змінити. Те саме стосується c_str()іdata() .

Швидкий google говорить про те, що багатопоточність є в основному причиною її фактичного заборони (не явно).

Пропозиція говорить:

Пропозиція

Ми пропонуємо безпечно виконати всі операції доступу до ітераторів та елементів.

Ми збільшуємо стабільність операцій навіть у послідовному коді.

Ця зміна ефективно відключає реалізацію копій за записом.

слідом за ним

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

Мотузки входять до складу STLPort та SGIs STL.


2
Проблема оператора [] насправді не є проблемою. Варіант const пропонує захист, а неконструктивний варіант завжди має можливість робити корова в той час (або бути справді божевільним і встановити помилку сторінки для її запуску).
Крістофер Сміт

+1 Переходить до проблем.
Ура та хт. - Альф

5
просто нерозумно, що клас std :: cow_string не був включений, з lock_buffer () і т. д. Є багато разів, я знаю, що нитка не є проблемою. найчастіше насправді.
Ерік Аронесті

Мені подобається пропозиція альтернативи, мотузки. Цікаво, чи є в наявності інші альтернативи та варіанти.
Вольтер

5

З 21.4.2 конструкторів базових рядків та операторів присвоєння [string.cons]

basic_string(const basic_string<charT,traits,Allocator>& str);

[...]

2 ефекти : Конструює об’єкт класу, basic_stringяк зазначено в таблиці 64. [...]

Таблиця 64 корисно документує, що після побудови об'єкта через цей (копіюючий) конструктор this->data()має значення:

точки на першому елементі виділеної копії масиву, на першому елементі якого вказано str.data ()

Існують подібні вимоги до інших подібних конструкторів.


+1 Пояснює, як C ++ 11 (принаймні частково) забороняє COW.
Ура та хт. - Альф

Вибачте, я втомився. Це не пояснює нічого більше, ніж те, що виклик .data () повинен викликати копіювання COW, якщо буфер загальнодоступний. І все-таки це корисна інформація, тому я дав змогу піднятися.
Ура та хт. - Альф

1

Оскільки зараз гарантується, що рядки зберігаються безперервно, і вам тепер дозволяється взяти покажчик на внутрішнє зберігання рядка (тобто & str [0] працює так, як це було б для масиву), зробити корисну COW неможливо реалізація. Вам доведеться зробити копію для занадто багато речей. Навіть просто використання operator[]або begin()на рядок без const вимагає копії.


1
Я думаю, що рядки в C ++ 11 гарантовано зберігатимуться постійно.
mfontanini

4
У минулому вам довелося робити копії у всіх тих ситуаціях, і це не було проблемою ...
Девід Родрігес - dribeas

@mfontanini так, але раніше їх не було
Дірк Холсоппл

3
Хоча C ++ 11 гарантує, що рядки гарантії є суміжними, що є ортогональним для заборони рядків COW. Рядок COW GCC є суміжним, тому чітко ваше твердження про те, що "неможливо зробити корисну реалізацію COW" є хибною.
Jonathan Wakely

1
@supercat, запит на зберігання резервного магазину (наприклад, зателефонувавши c_str()) повинен бути O (1) і не може кидати, і не повинен вводити перегони даних, тому дуже важко виконати ці вимоги, якщо ти лінько об'єднаєшся. На практиці єдиний розумний варіант - це завжди зберігати суміжні дані.
Джонатан Вейклі

1

Є корова basic_string заборонено на C ++ 11 та пізніших версіях?

Стосовно

" Чи я правильний, що C ++ 11 не допускає реалізацію на основі COW std::string?

Так.

Стосовно

" Якщо так, то це обмеження прямо вказано десь у новому стандарті (де)?

Майже безпосередньо, вимогами постійної складності для ряду операцій, які потребували б O ( n ) фізичного копіювання рядкових даних при реалізації програми COW.

Наприклад, для функцій-членів

auto operator[](size_type pos) const -> const_reference;
auto operator[](size_type pos) -> reference;

… Що в реалізації програми COW призведе до triggerзапуску копіювання рядкових даних, щоб розділити значення рядка, стандарт C ++ 11 вимагає

C ++ 11 §21.4.5 / 4 :

Складність: постійне час.

… Що виключає таке копіювання даних, а значить, і COW.

C ++ 03 підтримуються реалізації корови НЕ маючи ці вимоги постійної складності, і, за певних обмежувальних умов, що дозволяє дзвонити на operator[](), at(), begin(), rbegin(), end(), або rend()про визнання недійсних посилань, покажчики і Ітератор зі посиланням на строкових елементи, тобто , можливо , понесе Копіювання даних COW. Ця підтримка була видалена в C ++ 11.


Чи забороняється КР також через правила недійсності C ++ 11?

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

" Для рядка COW для виклику non- const operator[]потрібно буде зробити копію (та недійсні посилання), що заборонено [цитованим] пунктом вище [C ++ 11 §21.4.1 / 6]. Отже, більше не законно мати рядок COW в C ++ 11.

Це твердження є невірним та оманливим двома основними способами:

  • Це неправильно вказує на те, що constкопіювати дані COW потрібно лише непропозиційним постачальникам.
    Але також constпостачальникам елементів потрібно запустити копіювання даних, оскільки вони дозволяють клієнтському коду формувати посилання або покажчики, на які (в C ++ 11) пізніше не можна визнати недійсними через операції, які можуть викликати копіювання даних COW.
  • Це неправильно передбачає, що копіювання даних COW може призвести до недійсності посилань.
    Але при правильній реалізації копіювання даних COW, не розподіляючи значення рядка, робиться в момент, перш ніж з’являться будь-які посилання, які можуть бути визнані недійсними.

Щоб побачити, як працює правильна реалізація C ++ 11 COW basic_string, коли вимоги O (1), які роблять це недійсним, ігноруються, подумайте про реалізацію, де рядок може перемикатися між політикою власності. Екземпляр рядка починається з політики Sharable. Якщо ця політика активна, не може бути зовнішніх посилань на елементи. Екземпляр може переходити до унікальної політики, і він повинен це робити, коли потенційно створюється посилання на елемент, наприклад, із закликом до .c_str()(принаймні, якщо це створює вказівник на внутрішній буфер). У загальному випадку для кількох примірників, що ділять право власності на значення, це тягне за собою копіювання рядкових даних. Після цього переходу до унікальної політики екземпляр може переходити назад до Sharable лише операцією, яка скасовує всі посилання, наприклад, призначення.

Отже, хоча висновок цієї відповіді про те, що нитки КОР виключені, є правильним, пропоновані міркування невірні та сильно оманливі.

Я підозрюю, що причиною цього непорозуміння є ненормативна примітка у додатку С ++ 11 до додатка C:

C ++ 11 §C.2.11 [diff.cpp03.strings], про §21.3:

Зміна : basic_stringвимоги більше не дозволяють посилатися на рядки рядків
Обгрунтування: Недійсне визнання суттєво відрізняється від рядків, що рахуються посиланням. Ця зміна регулює поведінку (sic) цього Міжнародного стандарту.
Вплив на оригінальну функцію: Дійсний код C ++ 2003 може виконуватися по-різному в цьому Міжнародному стандарті

Тут обґрунтування пояснює головне, чому хтось вирішив усунути спеціальну підтримку COW C ++ 03. Таке обґрунтування, чому , не полягає в тому , як стандарт ефективно забороняє впровадження КОР. Стандарт забороняє КРС через вимоги O (1).

Коротше кажучи, правила інвалідизації C ++ 11 не виключають впровадження програми COW std::basic_string. Але вони виключають досить ефективну необмежену реалізацію COW у стилі C ++ 03, наприклад, у щонайменше одній із стандартних реалізацій бібліотеки g ++. Спеціальна підтримка COW C ++ 03 COW дозволила досягти практичної ефективності, зокрема, використовуючи constаксесуари для предметів, ціною складних правил щодо визнання недійсними:

C ++ 03 §21.3 / 5, який включає підтримку КРУ "для першого дзвінка":

Посилання, покажчики та ітератори, що посилаються на елементи basic_stringпослідовності, можуть бути визнані недійсними внаслідок таких застосувань цього basic_stringоб’єкта:
- Як аргумент для swap()нечленуючих функцій (21.3.7.8), operator>>()(21.3.7.9) та getline()(21.3. 7.9).
- Як аргумент до basic_string::swap().
- Функції виклику data()та c_str()учасника.
- Виклик непро- constфункцій - членів, за винятком operator[](), at(), begin(), rbegin(), end(), і rend().
- Після будь-якої з вищевказаних цілей , за винятком форм insert()і erase()які повертають ітератори, перший виклик , що не є constфункції - членів operator[](), at(), begin(), rbegin(),end(), або rend().

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


Що робити, якщо вимоги O (1) не дотримуються?

Якщо вимоги постійного часу C ++ 11 щодо, наприклад, operator[]не враховуються, то КОР basic_stringможе бути технічно здійсненним, але важким у виконанні.

Операції, які могли отримати доступ до вмісту рядка без копіювання даних COW, включають:

  • З'єднання через +.
  • Вихід через <<.
  • Використання в basic_stringякості аргументу стандартних функцій бібліотеки.

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

Крім того, реалізація може запропонувати різні нестандартні функції доступу до рядкового вмісту без ініціювання копіювання даних COW.

Основним ускладнюючим фактором є те, що в C ++ 11 basic_stringелемент доступу повинен викликати копіювання даних (не поділяючи рядкові дані), але він не повинен викидати , наприклад, C ++ 11 §21.4.5 / 3 " Кидає: нічого". І тому він не може використовувати звичайний динамічний розподіл для створення нового буфера для копіювання даних COW. Одним із способів цього є використання спеціальної купи, де можна зарезервувати без фактичного розподілу, а потім резервувати необхідну кількість для кожної логічної посилання на значення рядка. Зарезервувати та зняти резервування в такій купі може бути постійний час, O (1), і виділити суму, яку вже забронювали, може бути пам'ять noexcept. Для того, щоб відповідати вимогам стандарту, при такому підході, мабуть, знадобиться одна така спеціальна купа на основі резервування на окремий розподільник.


Примітки:
access constАксесуар товару запускає копіювання даних COW, оскільки він дозволяє клієнтському коду отримати посилання або вказівник на дані, які забороняється визнати недійсними при пізнішому копіюванні даних, ініційованому, наприклад, немеханічним constдоступом.


3
" Ваш приклад є хорошим прикладом неправильної реалізації для C ++ 11. Можливо, це було правильним для C ++ 03." Так , це суть прикладу . Він показує рядок COW, який був легальним у C ++ 03, оскільки він не порушує старі правила відключення ітератора та не є законним для C ++ 11, оскільки він порушує нові правила відключення ітератора. І це також суперечить твердженню, яке я цитував у коментарі вище.
Джонатан Уейклі

2
Якби ви сказали спільного доступу НЕ спочатку розділяє я б не стверджував. Сказати, що щось спочатку ділиться, просто заплутано. Спільний з собою? Це не те, що означає це слово. Але я повторюю: ваша спроба стверджувати, що правила інвалідації C ++ 11 ітератора не забороняють деякі гіпотетичні рядки COW, які ніколи не використовувались на практиці (і мали б неприйнятну продуктивність), коли вони, безумовно, забороняють тип рядка COW що використовувалося на практиці, є дещо академічним та безглуздим.
Джонатан Уейклі

5
Запропонований рядок COW цікавий, але я не впевнений, наскільки це було б корисно . Суть рядка COW полягає в копіюванні рядкових даних лише в тому випадку, якщо два рядки записані. Пропонована реалізація вимагає копіювання, коли відбувається будь-яка визначена користувачем операція зчитування. Навіть якщо компілятор знає лише його прочитане, він все одно повинен копіювати. Крім того, копіювання рядка Unique призведе до копіювання його рядкових даних (імовірно, до стану Sharable), що знову робить COW досить безглуздим. Тож без гарантій складності ви могли б написати ... дійсно хитру рядок COW.
Нікол Болас

2
Тож, поки ви технічно правильні, що гарантії складності - це те, що заважає вам писати будь-яку форму COW, це насправді [basic.string] / 5 не дозволяє вам написати будь-яку справді корисну форму рядка COW.
Нікол Болас

4
@JonathanWakely: (1) Ваша цитата - це не питання. Ось питання: "Чи правильно я вважаю, що C ++ 11 не допускає реалізацію std :: string на основі COW? Якщо так, чи це обмеження прямо вказано десь у новому стандарті (де)? " (2) Ваша думка, що КОР std::string, якщо нехтувати вимогами O (1), виявиться неефективним, - це ваша думка. Я не знаю, якою може бути вистава, але я вважаю, що це твердження висловлюється більше для його відчуття, для вібрів, які він передає, ніж для будь-якої відповідності цій відповіді.
Ура та хт. - Альф

0

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

Я встиг сьогодні спробувати це для простого тесту порівняння: карта розміром N, введена рядком / коровою, при цьому кожен вузол містить набір усіх рядків на карті (у нас є кількість NxN об’єктів).

З рядками розміром ~ 300 байт і N = 2000 корів трохи швидше і використовують майже на порядок менше пам'яті. Дивіться нижче, розміри в кб, біг b - з коровами.

~/icow$ ./tst 2000
preparation a
run
done a: time-delta=6 mem-delta=1563276
preparation b
run
done a: time-delta=3 mem-delta=186384
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.