All streams
Search
Write a publication
Pull to refresh
51
0.2
Valentin Nechayev @netch80

Программист (backend/сети)

Send message
> то проверяется пониманию соискателем преобразования чисел между системами счисления.

Я бы сказал чуть иначе — ответ (умение преобразовывать) не самое главное, главное — включится ли у кандидата «тревожная лампочка» при виде этого кода. Если он скажет в первые несколько секунд «ой, восьмеричка, зачем мне это <допустимое в обществе грубое ругательство>?» — цель уже достигнута, а пересчитывать 0123 в более привычное — можно и на калькулятор возложить. Возможен другой вариант реакции, главное, что он заметил «подставу».

> сколько встречался с языками программирования — восьмеричная система счисления

К счастью, эта диверсионная черта встречается далеко не всегда, и в новых языках стараются от неё отказываться.
> 5.1.2.3 Program execution
Ну, не этот пункт, но я нашёл нужное совсем рядом. Спасибо. Стандарты такого рода, увы, очень неудобны для чтения.

> Volatile разрушает оптимизации так же, как вызов функции без исходников (за исключением известных компилятору типа memcpy) — а вы его во внутренний цикл пихать хотите…

Простите, про внутренний цикл и т.п. — это Ваши необоснованные домыслы. Я нигде не предлагал использовать его в критических по скорости местах; на практике я бы там скорее применил ассемблерные вставки (типа movd на x86), не надеясь на оптимизации всяких memcpy(). В остальном я всё время вёл речь про то, как сделать относительно быстро при надёжной гарантии (и без закапывания в особенности каждого компилятора). Ответы я в итоге получил, нового уже, скорее всего, тут не будет совсем.

> Извините, но все эти сущности являются частью языка C. Такой же неотьемлемой, как volatile или memcpy.

Не во freestanding случае. Я его учитываю как важный.

В общем, спасибо, можно закрывать ветку.
> Не нужно. -fbuiltin-memcpy нужно задавать только если кто-то зачем-то сказал -fno-builtin.

-ffreestanding включает -fno-builtin.

> Если вы задали -O0, то это значит что скорость работы программы вас не волнует, уж извините.

Всё равно волнует, не извиню. Когда что-то просто не оптимизируется — это одно, а когда явно включается противодействие — это уже другое.

> Если вам нужна скорость — используйте хотя бы -Og.

-Og появился только в 4.8 и отсутствует вообще в Clang.

> Вот если бы вариант с volatile оказался не таким же, а быстрее — было бы о чём говорить.

При 4.2 и -O0 он быстрее — за счёт отсутствия тяжёлого вызова библиотечной функции.

> Со стороны людей, которые явно выключают оптимизации, а потом жалуются на то, что программы работают межденно. Извините, но я с этим миром стараюсь не пересекаться.

Когда появится уровень оптимизации, который никак не вредит отладке (действия не переходят границу исходной строки) — я займу такую же позицию. Пока что даже -Og такого не даёт, хоть и заметно приближается.
> отказаться от возможности переносить обращения к другим переменным через точку в программе, где вы вставили обращение к volatile.

Прошу обоснования цитатами из стандарта.

> Чем этот вариант хуже, чем volatile?

Завязкой на сущности, которые заметно выходят за пределы собственно обстановки рантайма C.
Ну, во-первых,
> Современные компиляторы — знают о memcpy и могут вызывать проблемы с алиасингом. Volatile не нужен, нужно использовать memcpy.

это знание отрубается нафиг простым включением freestanding :) после чего надо ещё искать, как ему обратно объяснить, что он должен знать, что такое memcpy.
Но это несколько побочная ветвь, я её упоминаю только для того, что Ваша линейная картина далеко не такая линейная.

> Чуть более старые компиляторы — знают о memcpy, но не используют информацию о типах для алиасинг оптимизации. Volatile не нужен, можно использовать memcpy.

Вот GCC 4.2 — он ещё идёт в текущей FreeBSD как альтернатива clangʼу. Он уже умеет -fstrict-aliasing, но при -O0 он на Вашем варианте исходной функции явно зовёт библиотечную memcpy(), а не встроенный вариант. Значит, уже реальный пример картины наоборот — алиасинг есть, «хорошего» memcpy нет.

Кстати, gcc всех версий у меня показал в варианте с memcpy, что float->int он копирует через регистр, а финальный int->float через стек, например вот для 6.4.0 и -O3:

FastInvSqrt:
        movd    %xmm0, %edx
        movl    $1597463007, %eax
        sarl    %edx
        subl    %edx, %eax
        movl    %eax, -4(%rsp)
        movss   -4(%rsp), %xmm0
        ret


и запись результата такая же, как если бы была через volatile, через union…

> Вот найдёте хоть один компилятор где volatile нужен, спасает и работает быстрее, чем memcpy — будет о чём поговорить.

Вот, считайте, уже нашёл — GCC 4.2 без -O. Если Вы не считаете этот вариант значимым именно из-за неоптимизации — я не могу с этим согласиться, скорость при отладке тоже имеет значение, и получать заведомые тормоза на ровном месте как-то не хочется. И на «не медленнее» тоже примеры уже показал.

Так что как минимум с одной стороны наш мир плоский :)
> Volatile — это ошибка в дизайне C. Его невозможно реализовать корректно и эффективно

Его возможно реализовать корректно: данное в памяти надо читать, не обращая внимание на все предыдущие идеи о том, что там должно быть, и на последующие о том, что туда записано.
Его возможно реализовать эффективно: надо всего лишь прочитать и записать. Ничего больше.
«Ошибки» здесь нет.

> Даже в ядре, при общении с железом (а это то, ради чего придуман volatile) его рекомендуют избегать

Я в курсе этого сообщения. Там возражения против него рассматриваются исключительно в свете синхронизации общения с другими параллельно исполняющимися задачами. Для этого он действительно не нужен.
Но там не рассматривается тема проблем неожиданного алиасинга.

> там, где важна скорость

Вы зациклились на понятии скорости и ничего другого не желаете слышать. Я же всё время говорю об обеспечении _корректности_ операций такого «сомнительного» типа. И только после обеспечения корректности, считаю, можно начинать рассматривать тему скорости.
Давайте, пока Вы не согласитесь с последним абзацем, не будете мне отвечать:) потому что иначе мы впадём в вечный цикл. Молчание будет достаточным индикатором несогласия.
> Вместо одной инструкции вы получаете 4 — неужели вы думаете, что это не отразится на скорости работы программы?

Я в курсе, спасибо.

> Ну честное слово, volatile — это ну вот совсем не то, что вы хотите увидеть в кусочке программы, который вы всеми правдами и неправдами пытаетесь ускорить!

Позвольте уж каждому на месте решать, насколько и что ему надо ускорять, и является ли вообще скорость тут проблемой. «Tools, not policy.»
А в реальности — скорее всего будут применены результаты сравнения нескольких методов для конкретного таргета и компилятора, даже если лучшим окажется то, что казалось худшим.
> Ответ простой: конечно же volatile работает.

Спасибо за прямой ответ. Жаль, что его буквально пришлось вытягивать.

> Однако в качестве побочного эффекта — возможно замедление программы. Не на проценты — в разы.

Какое будет замедление в конкретном случае — позвольте судить именно по этому случаю, а не предсказывать, не зная, что где и как работает.
Меня интересовало, подходит ли этот метод на пусть медленную, но гарантированную базу. А уже имея её в запасе — можно выбирать и более эффективные методы.
> Он, собственно, работает во всех рапространённых компиляторах и является единственным переносимым способом сделать то, что вы хотите

И ещё раз. Тут Вы пишете, что это _единственный_ (сами выделили) переносимый. А выше пишете

> Средство надёжное, спору нет, просто… несколько радикальное.

Если надёжное — то тоже работает или переносимое? Или нет? Я очень прошу дать прямой ответ, без всяких отклонений на bit_cast и тому подобное.
Регистры нужны не потому, что они не L1, а потому, что они не память — их мало (проще адресовать), и они не ограничены требованием представления в памяти (например, можно переименовывать внутри процессора, или когда память ещё не настроена). К обсуждаемому это не относится.
> Мне у ADSP нравится: один и тот же регистр может быть float, а может быть int в зависимости от имени, по которому к нему обращаешься. :)

Вот народ из RISC-V разработки пишет:

>> We considered a unified register file for both integer and floating-point values as this simplifies software register allocation and calling conventions, and reduces total user state. However, a split organization increases the total number of registers accessible with a given instruction width, simplifies provision of enough regfile ports for wide superscalar issue, supports decoupled floating-point-unit architectures, and simplifies use of internal floating-point encoding techniques. Compiler support and calling conventions for split register file architectures are well understood, and using dirty bits on floating-point register file state can reduce context-switch overhead.

Аргументы, как для не-крошечных систем, вполне разумны.

> ЗЗЫ: Интел — отстой ;)

Я согласен во многом, но не в этом случае разделения целочисленных и {x,y,z}mm регистров.
> В итоге при нуедачном стечении обстоятельств

А как может возникнуть такое стечение обстоятельств, если содержимое не пойдёт дальше L1?
Затраты на вытеснение чего-то другого в DRAM? Ну так их лучше считать размазанными по всей программе, а локальные переменные всё равно возле вершины стека и наверняка уже давно кэшированы.
Другого источника с ходу не вижу.
> Типичная ситуация когда вы думаете о компиляторе как о живом, почти разумном, существе. Который пытается понять смысл вашей программы и сделать её лучше. Чего и в помине нету.

Мысленное одушевление столь сложной сущности это вообще-то неизбежно для человека. И смысл он вполне может понять, в простых случаях. Но я не это имел в виду.

> А когда вы используете плюс то из-за переноса могут быть разные странные эффекты.

Вот именно.
Ну вот я считаю, по своему прочтению доступных данных (стандарты и final drafts), что решает, хоть и чуть дороже (за счёт обязательного размещения в оперативной памяти, регистры не годятся). А коллеги Halt и khim этот метод не отвергают, но просто игнорируют; зато настаивают, что memcpy в современном мире сам по себе достаточен, и даёт даже работу через регистры, где компилятор это сумел понять. Я понимаю — там, где вариант с memcpy работает, он эффективнее. Но прямого ответа нет…
> Плохо тестировали. Clang так умеет, а для GCC нужно чуть-чуть по другому написать.
У меня основной GCC пока 4.8, а Clang — района 3.8. Даже 4.0 эту свёртку ещё не умеет. Но за идею про "|" спасибо, тут было неочевидно, что это ещё может на него повлиять :)
> Так что если у меня написано i = *(uint32 *)&f то оно проверено и это никогда уже не поменяется.

Проблема в том, что тут стандарты «плывут» и компиляторы вслед за ними. В сверхбольшом проекте каждая смена версии компилятора выявляет пачки проблемных мест. Вот в этом случае GCC 4.8 внёс по сравнению с 4.7 сильно больше агрессивности в том, что до того в принципе уже считалось существующим в нём и дало неожиданность на ровном месте.
И если ваше «никогда не поменяется» включает в себя фиксацию версии компилятора — ok, но тогда вы иначе ограничены в развитии. Если нет — будьте готовы к сюрпризам.

> Но вообще интересно, как «по-правильному» передавать информацию в кодограммах между двумя машинами с заранее неизвестными компиляторами.

Если речь о взаимодействии протоколами, то тут есть несколько универсальных, но весьма громоздких подходов.
Например, стандарт обещает отсутствие проблем с алиасингом при доступе через char*. Если вам надо прочитать uint32_t, то гарантированно остаётся только что-то вроде

unsigned char *p;
return ((uint32_t)p[3]<<24) + ((uint32_t)p[2]<<16) +
  ((uint32_t)p[1]<<8) + (uint32_t)p[0];

(заметим, что конверсию в uint32_t тут нужно делать явно! по умолчанию оно стремится расширить в signed int, если влезают все значения, а не в unsigned int — ещё одна засада...)

Сможет компилятор понять, что это можно на x86/ARM/MIPS/etc. превратить в mov в регистр — не знаю заранее. В моих тестах сейчас ни GCC, ни Clang этого не сумели. Но можно хотя бы зависимость от платформы вписать в #ifdef.

> А ещё интереснее на архитектурах, где на одной стороне байт 32-битный.

А данные из сети поступают на такие машины как? Я думаю, если интерфейс соответствует BSD sockets, то в один машинный 32-битный байт укладывается 1 сетевой октет (потеря 75% места, а что поделаешь). Тогда код взятия 2 абзацами выше — будет работать, и аналогичная обратная раскладка — тоже. А вот прямая запись в unsigned-поле в предположении, что в нём 4 байта — уже не сработает.
Уточнение про ЯВУ — не во всех все такие правила (например, для сдвигов нет гарантий «как N раз по 1 биту»; это специфика Go). Речь про общее направление и режим по умолчанию. Но оно достаточно показательно. Обычно мы ожидаем меньше «умничания» от компилятора для языка более низкого уровня, и больше — для высокого (вплоть до изменения алгоритма), здесь же наоборот.
Стандарты C, C++ описывают некий общий вариант, в котором жёстко не прописаны ни IEEE754, ни двоичная иерархия размеров, ни даже представление чисел в дополнительном коде. Вот в C99:

For signed integer types, the bits of the object representation shall be divided into three groups: value bits, padding bits, and the sign bit. There need not be any padding bits; there shall be exactly one sign bit. Each bit that is a value bit shall have the same value as the same bit in the object representation of the corresponding unsigned type (if there are
M value bits in the signed type and N in the unsigned type, then M ≤ N ). If the sign bit is zero, it shall not affect the resulting value. If the sign bit is one, the value shall be modified in one of the following ways:
— the corresponding value with sign bit 0 is negated (sign and magnitude);
— the sign bit has the value −(2^N) (two’s complement);
— the sign bit has the value −(2^(N−1)) (one’s complement).


Читая это, мне реально интересно, где они находят машины, у которых для данного описания M <= N, или где есть что-то кроме дополнительного кода для представления отрицательных. Возможно, Вы, если описываете себя как embedded специалист, назовёте пару имён?

Видимо, для тех, кто устал от этого всего, в Java, C#, Go сделано (цитирую по Go)

> The value of an n-bit integer is n bits wide and represented using two's complement arithmetic.
(никаких тебе «знаковый может быть у́же беззнакового»)
> In a function call, the function value and arguments are evaluated in the usual order.
(тут есть отсылка на вычисление слева направо; никаких «в любом порядке, как нам удобно», как в C)
> Shifts behave as if the left operand is shifted n times by 1 for a shift count of n.
(никаких «если сдвиг на величину большую либо равную ширине сдвигаемого, результат не определён»)
> when evaluating the operands of an expression, assignment, or return statement, all function calls, method calls, and communication operations are evaluated in lexical left-to-right order.
(слева направо, считаем, в большинстве случаев)
> For unsigned integer values, the operations +, -, *, and << are computed modulo 2^n, where n is the bit width of the unsigned integer's type.
(тут так же, как в C)
> For signed integers, the operations +, -, *, and << may legally overflow and the resulting value exists and is deterministically defined by the signed integer representation, the operation, and its operands. No exception is raised as a result of overflow. A compiler may not optimize code under the assumption that overflow does not occur. For instance, it may not assume that x < x + 1 is always true.
(принципиальное отличие от C; как если бы gcc вызван с -fwrapv)

возможно, результирующий код и не так эффективен, но зато надёжен в плане отсутствия эффектов, неожиданно вышибающих почву из-под ног.

И struct aliasing rule там тоже нет, насколько я знаю.

Если бы можно было гарантированно и по стандарту получать подобные ограничения в C, этим бы пользовалось >90% его пользователей, и им бы было несущественно, что от этого они теряют 10-20-30% скорости. Но сейчас C развивается немножечко так в другую сторону :(
> Как минимум, автор данного метода заложился на то, что представление плавающих чисел будет именно таким:

Да, но IEEE754 сейчас чуть более, чем везде. А где есть другие варианты (IBM zSeries, VAX) — IEEE754 тоже есть.

> Да и представление целых чисел бывает разное: little-endian и big-endian.

На платформах с целым LE плавающие тоже в LE, и наоборот.
Исключений пока не видно.

Вы правы в том, что этот трюк платформенно-зависим. Изначально он предполагался исключительно для x86. На другие платформы его надо переносить с проверкой этих условий. Но сейчас Вы вряд ли найдёте платформу общего назначения (не какой-то специальный embedded), где бы это не работало.

Это точно так же, как с дополнительным кодом для отрицательных целых (в английском — жаргонное twoʼs complement) — C ещё допускает иные варианты, а вот Java, C#, Go — нет, они реализуют только такой вариант.

> По хорошему этот код надо предаварить проверкой, выполняемой при компиляции

Думаю, в кроссплатформенных версиях так и делают. Я бы делал. Но шансы, что сработает защита, считаю нулевыми :)

Information

Rating
2,692-nd
Location
Киев, Киевская обл., Украина
Date of birth
Registered
Activity