Pull to refresh

Comments 21

>> Как и в предыдущем посте, это не гарантирует от оптимизации, потому что сами данные могут и не иметь квалификатора volatile (операция доступа по указателю дает lvalue, тип которой не влияет на сами данные)

В вашем примере как раз данные и имеют квалификатор volatile, а указатель на них — нет. Соответственно, при разыменовании у вас будет volatile lvalue, и компилятор обязан её прочитать.
lvalue имеет квалификатор volatile, а сама переменная — в зависимости от того, как она была объявлена.
В данном случае у вас объявлен не-volatile указатель на volatile-данные. Что, собственно, и требуется. Т.е. в этом варианте компилятор обязан прочитать все элементы, тут нет никаких других вариантов.
Подскажите, пожалуйста, где именно в Стандарте написано, что при манипуляции с переменной через volatile lvalue компилятор обязан обращаться с ней так, как будто сама переменная объявлена volatile.
В Стандарте в контексте volatile вообще нет разговора о переменных — там используется термин volatile object. Конкретно, C++11 1.9[intro.execution]/8:
The least requirements on a conforming implementation are:
— Access to volatile objects are evaluated strictly according to the rules of the abstract machine.

и /12:
Accessing an object designated by a volatile glvalue (3.10), modifying an object, calling a library I/O function, or calling a function that does any of those operations are all side effects, which are changes in the state of the execution environment. Evaluation of an expression (or a sub-expression) in general includes both value computations (including determining the identity of an object for glvalue evaluation and fetching a value previously assigned to an object for prvalue evaluation) and initiation of side effects.

Вы пропустили вот эту важную формулировку из /8:
These collectively are referred to as the observable behavior of the program.
В /8 говорится про необходимость обеспечить выполнение операций доступа к volatile objects (objects — это переменные (1.8/1)), эти операции относятся к наблюдаемому поведению, которое необходимо сохранять, а в /12 говорится, что определенный набор действий относится к побочным эффектам, про их относимость к наблюдаемому поведению и необходимость их сохранить ничего не сказано.
Переменные — это объекты, но объекты — это далеко не только переменные. «An object is a region of storage».
Да, но lvalue — это не object и не region of storage, в 3.10 говорится, что lvalue designates an object (обозначает объект).
Тут все упирается в несколько мутное определение понятия object. С одной стороны, написано так:

«An object is created by a definition (3.1), by a new-expression (5.3.4) or by the implementation (12.2) when needed.»

При этом 12.2 — это исключительно temporaries, поэтому про них можно пока забыть.

С другой стороны, если есть вот такой вот код:
int* p = (int*)malloc(sizeof(int));

То в нем *p — это, определенно, объект (точнее, lvalue, которая его обозначает) — хотя ни объявления, ни new здесь не было.

Но есть и 3.8/1, где сказано:

«The lifetime of an object of type T begins when:
— storage with the proper alignment and size for type T is obtained, and
— if the object has non-trivial initialization, its initialization is complete.»

Очевидно, что malloc(sizeof(int)) выделяет память достаточного размера и выравнивания для int. Т.е. вроде как у нас есть int. Но эта память имеет размер и выравнивание, подходящие и для const int, и для volatile int, а на 32-битных реализациях — и для long, и для указателей самых разных типов. Возникает вопрос: сколько объектов на самом деле «создал» malloc?

На эту тему на comp.std.c++ периодически случаются холивары. Распространено мнение, что это такая как бы квантовая штука — т.е. там одновременно разом существуют объекты всех POD-типов, для которых это валидный storage (размер + выравнивание). Т.е. такой неявный union с соответствующей семантикой, в котором, как и в случае с обычным, можно работать только с объектом одного типа за раз, за исключением случаев, оговоренных семантикой union'ов — разница в cv qualifiers, структуры с common initial sequence и т.д.

Если принять это, то в вашем случае там «существует» объект типа volatile char, который и читается через указатель соотв. типа.
Вот. Мутное определение, холивары и «если принять». Раз в Стандарте не прописано ясно и четко, как вы можете доказать, что volatile lvalue обязывает обращаться с соответствующим объектом как будто тот имеет квалификатор volatile?

Нет, я не говорю, что ваши рассуждения лишены здравого смысла, но пока что-то не прописано в Стандарте, утверждать, что именно так правильно и все реализации должны именно так работать, нельзя.
Проводился ли вообще анализ того, какие оптимизации делают другие компиляторы?

Скажем, LLVM версии выше 3.0 должна детектировать magic как переменную редукции. Соответственно, использовать векторные инструкции для операции свертки. Условие в цикле тоже вполне себе векторизуется. Так что очень странно, что код (по вашим словам) не был оптимизирован.

Подробнее можно почитать здесь: llvm.org/docs/Vectorizers.html
В данном случае векторизация кода не страшна — для блоков одного размера векторизованный код будет всё равно работать одинаково долго.
Я понимаю, меня просто удивило почему в листинге не было этого видно. Одно дело, если специально запрещали векторизацию, другое если нет.
Машинный код в посте получен Visual C++9 исключительно с целью сравнить варианты с break и без.
В данном случае векторизация кода не страшна — для блоков одного размера векторизованный код будет всё равно работать одинаково долго.
Зависит от реализации. Даже если magic — переменная редукции, ничто не помешает добавить проверку результатов вычисления на части массива magic_for_array_part == UCHAR_MAX и быстрый возврат результата, потому что такая проверка на результат не повлияет. Будет ли от этого выгода и сделает ли так конкретный компилятор — вопрос, но техническая возможность есть.
А чего переживать о снижении производительности на пару процентов? Это функция ведь не будет применяться повсеместно — только для каких-нибудь паролей\хешей, которые не так уж и часто надо сравнивать.
Насколько мне известно, такие решения обычно принимают на основании результатов профилирования или других измерений, а не на основании субъективной оценки переживаний.
Вообще конечно сама идея бредом попахивает. С тайминг-атаками нужно бороться квантованием времени (ну, например, все чувствительные к таким векторам атак ответы функций класть в очередь по времени и отдавать с задержкой не менее 1 мс или там 100 мс).
А сознательно грузить сервер ненужной работой… гринписа на них не хватает.
Этот подход подойдет для системы, у которой атакующий может измерять только время ответа. Если атака идет на систему вроде смарт-карты, у которой атакующий может также измерять энергопотребление, ситуация усложняется — переход от активных вычислений к пассивному ожиданию, возможно, будет сопровождаться заметным изменением энергопотребления, в этом случае осуществима та же атака по времени, но время нужно будет измерять не до момента выдачи ответа, а до момента снижения энергопотребления.
Был у меня случай похожий, когда программист одного прикладного проекта написал тесты, но счётчик цикла, как оказалось по незнанию, «заволотайлить» забыл, что в итоге привело бы к очень грустным последствиям, благо мну этот недочёт заметил сразу.

Мораль вообще очень проста, хочешь что бы код делал именно то, что ты написал, используй volatile.
Sign up to leave a comment.