
![]() |
Наши проекты:
Журнал · Discuz!ML · Wiki · DRKB · Помощь проекту |
|
ПРАВИЛА | FAQ | Помощь | Поиск | Участники | Календарь | Избранное | RSS |
[3.17.156.160] |
![]() |
|
![]() |
Сообщ.
#1
,
|
|
Подскажите, как указать в асм-вставке GCC, что входной регистр изменяется?
Грубо говоря, ![]() ![]() asm volatile( "cld\n\t" "rep movsl\n\t" : : "S"(src), "D"(dst), "c"(count) ); В доке пишут, что нет способа указать, что входной регистр изменяется, если не указать его в списке выходных параметров. Но что мне там указывать? Я же не могу написать в списке выходных параметров просто "=S", "=D", "=c" без скобок или с пустыми скобками. Есть такие мысли только: 1. Указывать во входных просто "r" и в асме писать mov %0, %%esi и т.д. Но это тоже как-то тупо (лишние пересылки туда-сюда, и кстати, не факт, что получится, может тупо не хватить регистров, если их будет больше, чем 3). 2. Сохранить и восстановить эти регистры (push/pop) внутри асм-вставки, но это тоже лишние танцы с бубном. И тут ещё один вопрос рождается: если я указал в clobbers callee-save регистры, мне же не нужно их сохранять/восстанавливать, верно (точнее говоря, я надеюсь), компилятор сделает это сам? |
![]() |
Сообщ.
#2
,
|
|
Ну там же ниже приведён пример для "An x86 example where the string memory argument is of unknown length." для repne scasb Чем не устроил? И вообще, оно надо-то?
|
![]() |
Сообщ.
#3
,
|
|
Qraizer, приведённый пример не подходит. Там указывается "+D" в output'е, потому что это значение нужно и прочитать, и записать. А мне записывать результат никуда не нужно. С параметром "=c" (и далее "0") аналогично. А параметр "a" не меняется вообще.
Я же спрашиваю про ситуацию, когда нужно прочитать, изменить, но результат никуда записывать не надо. Кроме того, я не понимаю, зачем там вообще используется параметр "m" (*(const char (*)[]) p). Для чего он нужен, ты понимаешь? Пока найден только такой вариант: ![]() ![]() void *dummy1, *dummy2, *dummy3 asm volatile( "cld\n\t" "rep movsl\n\t" : "=S"(dummy1), "=D"(dummy2), "=c"(dummy3) : "S"(src), "D"(dst), "c"(count) ); Но код становится не очень красивым в итоге. Я не понимаю, почему нельзя было разрешить указывать в clobbers параметры input. Очень странно, конечно. Добавлено И в примере с sumsq я не могу уловить смысл дублирования x и y: "m" (*x), "m" (*y). Прочитал несколько раз, но всё равно не доходит. |
![]() |
Сообщ.
#4
,
|
|
Я отнюдь не профи в этом синтаксисе, но кое-как разобрал ситуацию и пришёл к выводу, что тут клобберсов не нужно. Смотри, мож где наглючил, так что проверяй.
Итого, что-то типа:![]() ![]() int foo(const void *src, void *dst, int count) { asm volatile ( "cld\n\t" "rep movsl\n\t" : : "S"(src), "D"(dst), "c"(count) : "memory" ); } ![]() ![]() _foo: pushl %ebp movl %esp, %ebp pushl %edi pushl %esi movl 8(%ebp), %esi movl 12(%ebp), %edi movl 16(%ebp), %ecx /APP cld rep movsl /NO_APP popl %esi popl %edi popl %ebp ret |
![]() |
Сообщ.
#5
,
|
|
Усложним ему жизнь. Займём регистры. Для сравнения тут же рядом такая же функция, но без asm.
![]() ![]() int foo(const void *src, void *dst, int count) { register int a = 123, b = 456, c = 789; asm volatile ( "cld\n\t" "rep movsl\n\t" : : "S"(src), "D"(dst), "c"(count) : "memory" ); return a+b-c; } int bar(const void *src, void *dst, int count) { register int a = 123, b = 456, c = 789; /* asm volatile ( "cld\n\t" "rep movsl\n\t" : : "S"(src), "D"(dst), "c"(count) : "memory" ); */ return a+b-c; } ![]() ![]() _foo: pushl %ebp movl %esp, %ebp pushl %edi pushl %esi pushl %ebx movl $123, %eax movl $456, %edx movl $789, %ebx movl 8(%ebp), %esi movl 12(%ebp), %edi movl 16(%ebp), %ecx /APP cld rep movsl /NO_APP addl %edx, %eax subl %ebx, %eax popl %ebx popl %esi popl %edi popl %ebp ret ; ... _bar: pushl %ebp movl %esp, %ebp movl $123, %ecx movl $456, %eax movl $789, %edx leal (%ecx,%eax), %eax subl %edx, %eax popl %ebp ret Добавлено Ещё сильнее усложним жизнь. Сделаем много регистровых переменных. ![]() ![]() int foo(const void *src, void *dst, int count) { register int a = 123, b = 456, c = 789, x = 321, y = 654, z = 987; asm volatile ( "cld\n\t" "rep movsl\n\t" : : "S"(src), "D"(dst), "c"(count) : "memory" ); return (a+b-c) / (x+y-z); } int bar(const void *src, void *dst, int count) { register int a = 123, b = 456, c = 789, x = 321, y = 654, z = 987; /* asm volatile ( "cld\n\t" "rep movsl\n\t" : : "S"(src), "D"(dst), "c"(count) : "memory" );*/ return (a+b-c) / (x+y-z); } ![]() ![]() _foo: pushl %ebp movl %esp, %ebp pushl %edi pushl %esi pushl %ebx subl $16, %esp movl $123, %eax movl $456, %edx movl $789, %ebx movl $321, -16(%ebp) movl $654, -20(%ebp) movl $987, -24(%ebp) movl 8(%ebp), %esi movl 12(%ebp), %edi movl 16(%ebp), %ecx /APP cld rep movsl /NO_APP leal (%eax,%edx), %edx subl %ebx, %edx movl -16(%ebp), %eax addl -20(%ebp), %eax subl -24(%ebp), %eax movl %eax, -28(%ebp) movl %edx, %eax cltd idivl -28(%ebp) movl %eax, -28(%ebp) movl -28(%ebp), %eax addl $16, %esp popl %ebx popl %esi popl %edi popl %ebp ret ; ... _bar: pushl %ebp movl %esp, %ebp pushl %edi pushl %esi pushl %ebx subl $4, %esp movl $123, %edx movl $456, %eax movl $789, %ecx movl $321, %ebx movl $654, %esi movl $987, %edi addl %eax, %edx subl %ecx, %edx leal (%ebx,%esi), %eax subl %edi, %eax movl %eax, -16(%ebp) movl %edx, %eax cltd idivl -16(%ebp) movl %eax, -16(%ebp) movl -16(%ebp), %eax addl $4, %esp popl %ebx popl %esi popl %edi popl %ebp ret Добавлено Как видишь, не надо компилятору указывать то, о чём он и так в курсе. |
![]() |
Сообщ.
#6
,
|
|
Цитата Jin X @ Ну, он указывает, что исходные данные лежат в памяти по адресу p. Точнее – что инпутом является массив байтов неопределённого размера, на который указывает p. Зачем? А фик его знает. Возможно, оптимизатору это будет полезно знать, например, с точки зрения memory_order. Кроме того, я не понимаю, зачем там вообще используется параметр "m" (*(const char (*)[]) p). Для чего он нужен, ты понимаешь? Добавлено Цитата Jin X @ Это не дублирование. Во-первых, упомянутое memory_order может помочь в расстановке барьеров. В нашем коде "memory" в клабберсах служит вообще-то для того же, но слишком грубый. Во-вторых, они отдельно описывают значения, на которые ссылаются x и y, что может помочь оптимизатору при анализе окружающего контекста и генерации его кода. И в примере с sumsq я не могу уловить смысл дублирования x и y: "m" (*x), "m" (*y). Прочитал несколько раз, но всё равно не доходит. |
![]() |
Сообщ.
#7
,
|
|
Цитата Qraizer @ Откуда ж ему это будет известно, когда эти "S", "D", "c" указаны в инпутах? Тот факт, что esi, edi и ecx тоже меняются, компилятору и без нас известно посредством "S", "D" и "c". ![]() Цитата Qraizer @ Сейчас я тебе красоту покажу Ну разве не красота ![]() https://gcc.godbolt.org/z/d_TZNF ![]() ![]() long test(long src, long dst, long count) { __asm__ __volatile__ ( "cld\n\t" "rep movsq\n\t" : : "S"(src), "D"(dst), "c"(count) : "memory" ); return src + dst + count; } ![]() ![]() test(long, long, long): mov r8, rdi mov rcx, rdx mov rdi, rsi mov rsi, r8 cld rep movsq add r8, rdi lea rax, [r8+rdx] ret Добавлено "memory" на это никак не влияет. |
![]() |
Сообщ.
#8
,
|
|
Ради интереса заменил на
![]() ![]() asm volatile ( "cld\n\t" "rep movsl\n\t" : "=m"(*(int(*)[count])dst) : "S"(src), "D"(dst), "c"(count) ); ![]() ![]() _foo: pushl %ebp movl %esp, %ebp pushl %edi pushl %esi pushl %ebx subl $20, %esp movl $123, -16(%ebp) movl $456, %edx movl $789, %ebx movl $321, -20(%ebp) movl $654, -24(%ebp) movl $987, -28(%ebp) movl 12(%ebp), %eax movl 8(%ebp), %esi movl 12(%ebp), %edi movl 16(%ebp), %ecx /APP cld rep movsl /NO_APP addl -16(%ebp), %edx subl %ebx, %edx movl -20(%ebp), %eax addl -24(%ebp), %eax subl -28(%ebp), %eax movl %eax, -32(%ebp) movl %edx, %eax cltd idivl -32(%ebp) movl %eax, -32(%ebp) movl -32(%ebp), %eax addl $20, %esp popl %ebx popl %esi popl %edi popl %ebp ret ![]() Добавлено Цитата Jin X @ Гы. Значит ты не прав, что не считаешь его результатом asm-вставки. Как видишь, он спокойно юзает rdi, будто он не изменился. |
![]() |
Сообщ.
#9
,
|
|
Цитата Qraizer @ Х/з, edi после как не использовался, так и не используется, поэтому, видимо, он туда ничего и не пишет. У тебя уровень оптимизации какой?Что бы это значило? Цитата Qraizer @ AT&T ужасен, как так можно извратить имя cdq?cltd Цитата Qraizer @ Это иллюстрация того, что если регистра нет в outputs и clobbers, то компилер имеет право думать, что регистр не изменился. Значит ты не прав, что не считаешь его результатом asm-вставки. |
![]() |
Сообщ.
#10
,
|
|
Цитата Jin X @ Именно. Ровно так работает и C, согласись: когда ты не используешь переменную в выражении, она и не меняется. Та и вообще любой язык так работает. И никто не будет переписывать специфический фрагмент анализатора под специфический случай специфического процессора. Если тебе без разницы, что esi, с которым связан параметр src, меняется, забей, и пусть компилятор тоже забьёт. Если же не без разницы, тогда будь добр либо укажи на это компилятору аутпутом для этой переменной, либо сделай для неё копию, и меняй её. Это иллюстрация того, что если регистра нет в outputs и clobbers, то компилер имеет право думать, что регистр не изменился. Добавлено А так получается, что ты написал что-то типа ![]() ![]() while (--count != 0) *dst++ = *src++; Добавлено P.S. Как вариант, ты можешь руками написать весь асмовый код. Тогда и компилятору ничего объяснять не придётся. Добавлено Цитата Jin X @ Совсем забыл.И тут ещё один вопрос рождается: если я указал в clobbers callee-save регистры, мне же не нужно их сохранять/восстанавливать, верно (точнее говоря, я надеюсь), компилятор сделает это сам? Да. По идее он постарается не задействовать эти регистры вообще, но если не получится, то озаботится сам. Добавлено Вот, для сравнения: ![]() ![]() void* foo(const void *src, void *dst, int count) { asm volatile ( "movl src, %%esi\n\t" "movl dst, %%edi\n\t" "movl count, %%ecx\n\t" "cld\n\t" "rep movsl\n\t" : "=m"(*(int(*)[count])dst) : : "ecx", "esi", "edi" ); return (int*)dst + count; } void* bar(const void *src, void *dst, int count) { return (int*)dst + count; } ![]() ![]() _foo: pushl %ebp movl %esp, %ebp pushl %edi pushl %esi movl 12(%ebp), %eax /APP movl src, %esi movl dst, %edi movl count, %ecx cld rep movsl /NO_APP movl 16(%ebp), %eax sall $2, %eax addl 12(%ebp), %eax popl %esi popl %edi popl %ebp ret ; ... _bar: pushl %ebp movl %esp, %ebp movl 16(%ebp), %eax sall $2, %eax addl 12(%ebp), %eax popl %ebp ret |
![]() |
Сообщ.
#11
,
|
|
Цитата Qraizer @ Проще было бы разрешить писать в clobbers регистры, которые указаны в inputs. Иначе это лишние телодвижения и не очень красивый код.Если же не без разницы, тогда будь добр либо укажи на это компилятору аутпутом для этой переменной, либо сделай для неё копию, и меняй её. Цитата Qraizer @ В принципе, да. Это будет проще, пожалуй, чем объявлять фиктивные доп. переменные.Как вариант, ты можешь руками написать весь асмовый код. Тогда и компилятору ничего объяснять не придётся. Цитата Qraizer @ Зачем вот это тут? Тогда уж в clobbers это прописывать надо, т.к. outputs – это то, что компилятор должен записать, а тут мы всё делаем сами. "=m"(*(int(*)[count])dst) |
![]() |
Сообщ.
#12
,
|
|
Цитата Jin X @ Если это позволить, то привязки инпутов просто не смогут работать. Параметры asm – это не вызов подпрограммы с передачей аргументов, это прокси между двумя разными языками с разной грамматикой. Указав, что некий, скажем, esi привязывается к src, ты делаешь между ними альяс, и естественно, что esi, меняясь, меняет и src. Если ты об этом не сообщишь компилятору, ты его просто обманешь, так что на это либо нужно забить, если это применимо, либо придётся указать esi в аутпутах, чтоб не обманывать. Ты ж фактически хочешь, чтобы на C можно было писать x++, но без изменения x. Ну выйдет же, придётся писать x+1 или сделать копию x, которой уже и делать ++. Проще было бы разрешить писать в clobbers регистры, которые указаны в inputs. Иначе это лишние телодвижения и не очень красивый код. |
![]() |
Сообщ.
#13
,
|
|
Цитата Qraizer @ Не вижу в этом ничего естественного. Это же не ссылка (бескомпромиссно!), а некий аналог переменной, самостоятельной, хоть и на более физическом уровне. На примере той же функции – есть передача параметров по ссылке, а есть по значению. Почему нельзя этот же принцип использовать здесь? Я не вижу никаких технических проблем в этом. Я же могу связать регистр с переменной в outputs, но только на выходе, не связывая их на входе. Почему нельзя связать на входе, не связывая на выходе? Тем более, что я могу привязать один и тот же регистр в inputs и outputs к разным переменным, поэтому аналогия с привязкой тут не очень удачная.естественно, что esi, меняясь, меняет и src Цитата Qraizer @ Правильно, inputs – это и есть создание копии (по крайней мере, во многих случаях). Поэтому я хочу сделать y = x; y++, а мне говорят: "Неее, y = x и баста! А если хочешь y++, тогда будь добр потом сделать z = y, даже если тебе z будет не нужен". Причём неважно, используется ли этот y++ внутри или изменяется вхолостую.Ты ж фактически хочешь, чтобы на C можно было писать x++, но без изменения x. Ну выйдет же, придётся писать x+1 или сделать копию x, которой уже и делать ++. Cобственно, мы имеем то, что имеем и повлиять на это можем в лучшем случае создав запрос или самостоятельно исправив исходники (если их потом примут). |
![]() |
Сообщ.
#14
,
|
|
Цитата Jin X @ Та после экспериментов осталось. Можно ж ещё и так:Зачем вот это тут? Тогда уж в clobbers это прописывать надо, т.к. outputs – это то, что компилятор должен записать, а тут мы всё делаем сами. ![]() ![]() asm volatile ( "movl %1, %%esi\n\t" "movl %0, %%edi\n\t" "movl %[length],%%ecx\n\t" "cld\n\t" "rep movsl\n\t" : "=m"(*(int(*)[count])dst) : "m"(*(int(*)[count])src), [length]"m"(count) : "ecx", "esi", "edi" ); Добавлено Цитата Jin X @ Ещё раз "ещё раз". Это не передача параметров, это связывание двух разных представлений одной и той же сущности. Если что-то указано как инпут, но не указано как аутпут, значит оно инпут, и баста. Ты не можешь, имея два представления одной сущности, ожидать, что изменяя одно, оставишь другое неизменным. Jin X, ты же писал макросы? Как ты себе представляешь отвязку параметра макроса от его аргумента? Это же не ссылка (бескомпромиссно!), а некий аналог переменной, самостоятельной, хоть и на более физическом уровне. На примере той же функции – есть передача параметров по ссылке, а есть по значению. Почему нельзя этот же принцип использовать здесь? Я не вижу никаких технических проблем в этом. Добавлено Цитата Jin X @ "В большинстве случаев" так компилятору приходится поступать из-за ограничений инструкций целевого процессора. Тут, например, невозможно src из памяти сразу подать на вход movs, потому ему приходится его копировать в esi. И обратно, если это ещё и выход. К семантике asm это не имеет отношения, как её спроектировали, это другой вопрос. Возможно, что раньше, году эдак в 75-ом, её и можно было бы спроектировать иначе, но в 2020-ом нежелание это делать я вполне понимаю. И скорее наоборот, буду выступать против, если меня спросят, потому как даже представить себе не могу, сколько кода по всему миру может быть затронуто сайд-эффектами от смены дизайна. Правильно, inputs – это и есть создание копии (по крайней мере, во многих случаях). Добавлено Цитата Jin X @ Это конкретные специфические особенности конкретного специфического процессора в конкретном специфическом случае. Вероятно, делать универсальный asm в семействе GNU было не очень хорошей идеей, но минусов от этого решения всё-таки куда меньше плюсов. Единый движок оптимизатора, например, однозначно плюс, ибо в классических интелово-майрософтовых решениях любая asm-вставка нередко ломает оптимизацию всей функции. Как по-моему, в конкретных специфических случаях проще сделать копию, чем хакнуть дизайн. Несложно же привязать этот же инпут-параметр к другому объекту, объявив его аутпутом? Поэтому я хочу сделать y = x; y++, а мне говорят: "Неее, y = x и баста! А если хочешь y++, тогда будь добр потом сделать z = y, даже если тебе z будет не нужен". Причём неважно, используется ли этот y++ внутри или изменяется вхолостую. |
![]() |
Сообщ.
#15
,
|
|
Цитата Qraizer @ С чего ты взял, что это связывание? Если указано как инпут, пусть будет как инпут. Но почему нельзя ему быть в клобберах? См. ниже.Это не передача параметров, это связывание двух разных представлений одной и той же сущности. Если что-то указано как инпут, но не указано как аутпут, значит оно инпут, и баста. Ты не можешь, имея два представления одной сущности, ожидать, что изменяя одно, оставишь другое неизменным. Цитата Qraizer @ При чём тут макросы, Саш? ты же писал макросы? Как ты себе представляешь отвязку параметра макроса от его аргумента? ![]() В макросах идёт подстановка аргумента вместо параметра в таком виде, в каком его передали, и используется "как есть". Это как #define, даже не связывание, а просто подстановка. Здесь же это не макросы, не #define'ы, здесь в регистр заносится значение заранее. Даже когда ты пишешь mov %0,%%edx при input-аргументе "r"(x), то компилятор может сделать сначала mov x,%eax, а уже потом mov %eax,%edx. Не делает он это только тогда, когда x уже в регистре, и нет разницы какой регистр использовать: https://gcc.godbolt.org/z/NBQL9X А если есть разница, тогда оптимизация не срабатывает: https://gcc.godbolt.org/z/UipRRN Но он не станет делать mov x,%edx (без последующего mov %0,%%edx): https://gcc.godbolt.org/z/4nEAGx (заметь, тут включена оптимизация!) Повторюсь: почему ты считаешь, что регистр жёстко связывается с аргументом? Ведь если регистр указан и в inputs, и в outputs, то эта логика нарушается, ибо как можно связать регистр сразу с двумя аргументами? А если можно, значит в какой-то момент регистр отвязывается от input-аргумента, а в какой-то привязывается к output-аргументу. Почему же нельзя отвязать и не привязывать ни к чему? Цитата Qraizer @ Компилятор вообще ничего не знает о том, что происходит внутри asm-вставки, т.к. это не его дело. Я могу написать там любую абракадабру, и он схавает. Запнётся только асм-транслятор.В большинстве случаев" так компилятору приходится поступать из-за ограничений инструкций целевого процессора. Тут, например, невозможно src из памяти сразу подать на вход movs, потому ему приходится его копировать в esi. И обратно, если это ещё и выход. К семантике asm это не имеет отношения, как её спроектировали, это другой вопрос. Смотри: https://gcc.godbolt.org/z/xrhNZz, ошибки нет ![]() Для компилятора всё происходящее внутри – чёрный ящик. Он может только заменять %0 (и т.п.) на нужные значения и всё. Цитата Qraizer @ Нежелание делать что? Возможно, что раньше, году эдак в 75-ом, её и можно было бы спроектировать иначе, но в 2020-ом нежелание это делать я вполне понимаю. И скорее наоборот, буду выступать против, если меня спросят, потому как даже представить себе не могу, сколько кода по всему миру может быть затронуто сайд-эффектами от смены дизайна. ![]() ![]() Если добавить возможность указывать одни и те же регистры и в input, и в clobbers, старый код никуда не денется, потому что там нет input-регистров в clobbers. А в новом будет возможность сделать по-новому. Смотри: http://www.ibiblio.org/gferg/ldp/GCC-Inlin...y-HOWTO.html#s5 ![]() ![]() asm ("cld\n\t" "rep\n\t" "stosl" : /* no output registers */ : "c" (count), "a" (fill_value), "D" (dest) : "%ecx", "%edi" ); А ты говоришь о сайд-эффектах. Тут вон отсутствие обратном несовместимости просто! |
![]() |
Сообщ.
#16
,
|
|
Как я это всё понимаю?
В блоке asm есть: Как это работает? Компилятор: Соответственно, если в inputs указано "c"(x), а в outputs и в clobbers нет "c"/"ecx", значит компилятор полагает, что в ecx по прежнему лежит x. Иначе не полагает. Вот и вся логика. Ещё раз про clobbers К примеру, если мы указали в clobbers параметр "eax", а перед вставкой выражения на ассемблере в код компилятор занёс туда x, то при чтении значения из x после выражения на ассемблере он не может теперь использовать регистр eax, т.к. там уже может быть не x, а всё, что угодно. Если мы указали "memory", а перед вставкой выражения на ассемблере в код он занёс в eax значение x, то он опять не может использовать eax при чтении значения из x, т.к. значение x могло измениться, и в eax теперь будет устаревшее значение x, а не актуальное. Вот, собственно, и всё. К чему выдумывать какие-то дополнительные ограничения? Каждый блок параметров обрабатывается отдельно и причин, почему одна и та же связка не может быть и в inputs, и в clobbers, я не вижу. Возможно, это немного упрощённое описание, но сути оно не меняет. Если у меня где-то ошибка в логике, прошу указать где, в чём и почему ![]() |
![]() |
Сообщ.
#17
,
|
|
Jin X, тебе осталось совсем немного. К примеру, на
Цитата Jin X @ ты можешь дать ответ, если подумаешь и свяжешь воедино мои и свои слова.Я не знаю, в чём тут прикол, но вероятно, раньше такое делать можно было, но потом (зачем-то) убрали. Мне хочется понять: ЗАЧЕМ? P.S. Впрочем, это не отменяет того факта, что всё это домыслы. Однако выглядят в целом логично, потому отвергать их нет нужды. |
![]() |
Сообщ.
#18
,
|
|
Цитата Qraizer @ Я всё равно не нахожу ответа и логики в этом.ты можешь дать ответ, если подумаешь и свяжешь воедино мои и свои слова. Ты сравниваешь это с макросами и со связыванием двух сущностей (регистра и переменной, к примеру). Я не вижу причин, почему такая связь не может рушиться на выходе из asm-блока. Ведь clobbers рушат другие связи (указанный регистр с ранее связанной переменной, ещё до asm-блока, которая не фигурирует ни в inputs, ни в outputs). Ещё раз: Цитата Jin X @ Ведь если регистр указан и в inputs, и в outputs, то эта логика нарушается, ибо как можно связать регистр сразу с двумя аргументами? А если можно, значит в какой-то момент регистр отвязывается от input-аргумента, а в какой-то привязывается к output-аргументу. Почему же нельзя отвязать и не привязывать ни к чему? |