У меня есть некая структурка данных, и к ней надо обращаться то как к двум 32-битным беззнаковым интам, то как к одному 64-битному беззнаковому инту.
По сути, это union. Но, насколько я понимаю, через union по стандарту C++ этого делать нельзя, так как это нарушение strict aliasing и UB (записывать один тип, а читать другой, несовместимый, тип).
Есть очевидное решение через битовые операции, но я бы не хотел просто надеяться на компилятор — на то, что он избавится от битовых операций самостоятельно (и не будет делать сдвиги и маски, а будет загружать либо 64-битное слово в 64-битный регистр, либо два 32-битных слова в 32-битные регистры, или применит векторизацию), а хотел бы какое-то гарантированно эффективное решение, соответствующее при этом стандарту.
Еще у меня была мысль про буфер char'ов: по стандарту любой тип можно алиазить массивом чаров, тогда что если записать этот в массив std::uint64_t, а прочитать два std::uint32_t — будет ли это нарушением strict aliasing rule? Очевидный ответ — да, поскольку читается один тип, а записывается другой. А может и нет. Этот момент мне неясен.
Здравствуйте, Eeel, Вы писали:
E>У меня есть некая структурка данных, и к ней надо обращаться то как к двум 32-битным беззнаковым интам, то как к одному 64-битному беззнаковому инту.
А зачем? В том смысле, что на разных платформах могут быть разные эндианы, и одно и тоже 64-битное число таким образом будет мапиться на разные пар 32-битных.
Тебе надо физически с битиками памяти работать, или таки нужны логически старшие и младшие биты?
А ещё есть второй вопрос, зачем ты хочешь, обязательно читать/писать память, а не двигать битики? Вдруг двигать битики быстрее? Например, во многих x86 процах смесь 32-битных и 64-битных инструкций дорогая...
Зачем ты хочешь мешать оптимизатору?
В общем, если ты хочешь работать с памятью напрямую, то есть речь о чём-то железячном, то пиши, читай прямо по адресам. Всё равно это никак не переносимо, а если таки речь идёт об операциях с числами, то бери сдвиги/маски и вперёд...
Только не ясно, зачем тебе маски...
E>Еще у меня была мысль про буфер char'ов: по стандарту любой тип можно алиазить массивом чаров, тогда что если записать этот в массив std::uint64_t, а прочитать два std::uint32_t — будет ли это нарушением strict aliasing rule? Очевидный ответ — да, поскольку читается один тип, а записывается другой. А может и нет. Этот момент мне неясен.
Конечно, потому, что есть разыне endians, кроме того, ещё и с выравниванием могут быть проблемы, при таком подходе...
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Здравствуйте, Eeel, Вы писали:
E>По сути, это union. Но, насколько я понимаю, через union по стандарту C++ этого делать нельзя, так как это нарушение strict aliasing и UB (записывать один тип, а читать другой, несовместимый, тип).
Strictly speaking, reading a member of a union different from the one written to is undefined in ANSI/ISO C99 except in the special case of type-punning to a char*, similar to the example below: Casting to char*. However, it is an extremely common idiom and is well-supported by all major compilers. As a practical matter, reading and writing to any member of a union, in any order, is acceptable practice.
Здравствуйте, Erop, Вы писали:
E>А зачем? В том смысле, что на разных платформах могут быть разные эндианы, и одно и тоже 64-битное число таким образом будет мапиться на разные пар 32-битных.
Это в теории, а на практике будет только x86/64 — либо i7, либо Xeon. А big endian я просто не буду поддерживать, как как для этого нужно во всем коде специально делать поддержку и тщательно его весь именно для этого тестировать, что совершенно не стоит потраченных усилий. Так же, как я не собираюсь поддерживать системы меньше 64 бит.
Просто мне не хочется писать код, который в явном виде нарушает strict aliasing и, таким образом, зависит от опций компиляции. E>Тебе надо физически с битиками памяти работать, или таки нужны логически старшие и младшие биты?
Ну мне надо, например, параллельно сложить нижнее и верхнее 32-битные слова (нижнее с нижним, верхнее с верхним), так, чтобы при сложении нижнего не было переноса в верхнее. Но потом использовать это все как одно 64-битное слово.
E>А ещё есть второй вопрос, зачем ты хочешь, обязательно читать/писать память, а не двигать битики? Вдруг двигать битики быстрее? Например, во многих x86 процах смесь 32-битных и 64-битных инструкций дорогая... E>Зачем ты хочешь мешать оптимизатору?
Просто не хочется слепо надеяться на оптимизатор и писать заведомо бессмысленные битовые операции. Профессиональным подходом здесь было бы сделать бенчмарк, пожалуй, и именно так я и сделаю в итоге.
Меня просто еще интересует концептуальная возможность обходить strict aliasing rule, так как оно является очень большим ограничением.
E>В общем, если ты хочешь работать с памятью напрямую, то есть речь о чём-то железячном, то пиши, читай прямо по адресам. Всё равно это никак не переносимо, а если таки речь идёт об операциях с числами, то бери сдвиги/маски и вперёд...
Ну отмена переносимости кода мало помогает, так как сама по себе не отменяет правила strict aliasing в компиляторах. А от опций и версий конкретных компиляторов не хочется зависеть.
На самом деле, вопрос "зачем" — это несколько другой вопрос (как обычно) и слегка оффтопик.
Меня интересует принципиальный вопрос обхода strict aliasing rule и, прежде всего, ответ на следующий вопрос:
Еще у меня была мысль про буфер char'ов: по стандарту любой тип можно алиазить массивом чаров, тогда что если записать этот в массив std::uint64_t, а прочитать два std::uint32_t — будет ли это нарушением strict aliasing rule? Очевидный ответ — да, поскольку читается один тип, а записывается другой. А может и нет. Этот момент мне неясен.
Здравствуйте, Eeel, Вы писали:
E>Ну мне надо, например, параллельно сложить нижнее и верхнее 32-битные слова (нижнее с нижним, верхнее с верхним), так, чтобы при сложении нижнего не было переноса в верхнее. Но потом использовать это все как одно 64-битное слово.
Ну, например
a + b - (1<<32)&((a+b)^~(a^b))
И пусть себе там компилятор разбирается, как оптимизировать?
Ещё всякие векторные инструкции для этого рулят...
E>Просто не хочется слепо надеяться на оптимизатор и писать заведомо бессмысленные битовые операции. Профессиональным подходом здесь было бы сделать бенчмарк, пожалуй, и именно так я и сделаю в итоге.
Его надо делать на всех платформах... Стоит ли выигрыш затрат?
E>Меня просто еще интересует концептуальная возможность обходить strict aliasing rule, так как оно является очень большим ограничением.
Оно нужно для того, что бы развязать руки оптимизатору. Обходя его, ты неизбежно свободу оптимизатора снизишь (или код будет непредсказуемо работать)
E>Ну отмена переносимости кода мало помогает, так как сама по себе не отменяет правила strict aliasing в компиляторах. А от опций и версий конкретных компиляторов не хочется зависеть.
Если бы тебе нужна была именно работа с битиками памяти, например в конце стояло бы устройство отображённое на память, то как раз от опций компилятора ты бы тогда не зависел. Возможно, твой код в разных случаях интерпретировал бы ОДНИ И ТЕ ЖЕ битики по ОДНИМ И ТЕМ ЖЕ адресам, как разные числа, но когда дело доходило бы до аппаратных битиков, всё было бы правильно.
А тебе таки нужны числа, а не аппаратные битики. Вот с числами и работай. Так ндёжно, а обходить — мешать оптимизатору.
Иногда оптимизаторы лажают, конечно, но редко. И там, под конкретный оптимизатор, если задача очень нагруженная, можно сделать отдельную ветку кода, завязанную на особенности конкретного транслятора в конкретных настройках. Только это никак со strict aliasing rule не связано, оптимизаторы могут лажать сотнями способов...
E>
E>Еще у меня была мысль про буфер char'ов: по стандарту любой тип можно алиазить массивом чаров, тогда что если записать этот в массив std::uint64_t, а прочитать два std::uint32_t — будет ли это нарушением strict aliasing rule? Очевидный ответ — да, поскольку читается один тип, а записывается другой. А может и нет. Этот момент мне неясен.
Очевидный ответ верный. Просто сравни поведение на двух системах, где разный эндиан, и сразу поймёшь, что это так и есть...
Кроме того, при таком подходе ещё и проблемы с выравниванием добавятся...
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Здравствуйте, placement_new, Вы писали:
_>Strictly speaking, reading a member of a union different from the one written to is undefined in ANSI/ISO C99 except in the special case of type-punning to a char*, similar to the example below: Casting to char*. However, it is an extremely common idiom and is well-supported by all major compilers. As a practical matter, reading and writing to any member of a union, in any order, is acceptable practice.
Только оптимизацию может нагнуть...
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Здравствуйте, T4r4sB, Вы писали:
TB>memmove позволяет битово копировать данные разных типов без УБЭ. Но от огребания от индейца не спасёт, как уже сказали.
Тока это нагнёт оптимизацию ещё мощнее...
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Здравствуйте, Erop, Вы писали:
E>Здравствуйте, T4r4sB, Вы писали:
TB>>memmove позволяет битово копировать данные разных типов без УБЭ. Но от огребания от индейца не спасёт, как уже сказали.
E>Тока это нагнёт оптимизацию ещё мощнее...
А что за компилятор, который не умеет memmove?
Например, gcc и clang заменяют вызов функции на простое чтение из памяти даже при полностью выключенной оптимизации. Так что каст через memcpy может использоваться для борьбы с алиасингом. https://godbolt.org/g/mtEgIY
Здравствуйте, watchmaker, Вы писали:
W>А что за компилятор, который не умеет memmove? W>Например, gcc и clang заменяют вызов функции на простое чтение из памяти даже при полностью выключенной оптимизации. Так что каст через memcpy может использоваться для борьбы с алиасингом. https://godbolt.org/g/mtEgIY
ТС'у нужны операции, вроде сложить два 64-х числа без переноса между 31-м и 32-м битом.
Ты правда думаешь, что реализация через memmove не напряжёт оптимизатор?
Я, например, не в курсе, но как-то сомневаюсь, то оно хорошо соптимизируется...
Хотя вопрос, конечно, интересный.
Я думаю, что самый дружественный к оптимизации подход будет где-то тут:
a + b - ((1ULL<<32)&((a+b)^~(a^b)))
Но может, конечно, и через химию с памятью быстрее, но как-то мало верится. Например, не понятно, что скорее будет векторизовано...
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Здравствуйте, Erop, Вы писали:
E>Здравствуйте, watchmaker, Вы писали:
W>>А что за компилятор, который не умеет memmove? W>>Например, gcc и clang заменяют вызов функции на простое чтение из памяти даже при полностью выключенной оптимизации. Так что каст через memcpy может использоваться для борьбы с алиасингом. https://godbolt.org/g/mtEgIY
E>ТС'у нужны операции, вроде сложить два 64-х числа без переноса между 31-м и 32-м битом.
Извини, но я отвечал только на выпад в сторону memcpy/memmove. Насколько я понял, ТС больше интересуется правилами алиасинга, а не конкретной задачей.
Так то, конечно, для задачи сложения двух чисел, такое использование memcpy выглядит избыточном. Особенно в свете ориентации ТС на архитектуру x86, ибо там уже лет 20 есть инструкция paddd, которая делает ровно то что нужно — складывает у двух 64-х битных чисел нижние и верхние половинки без переноса между ними. Если уж была бы важна скорость, то стоит просто этой инструкцией воспользоваться (например через соответствующий интринсик).
Здравствуйте, Erop, Вы писали:
E>Тока это нагнёт оптимизацию ещё мощнее...
Нет, нормальные компиляторы нормально это воспринимают, а вот хвалёный ИЦЦ начинает нести бред при виде меммува, это да. Это гениально, оптимизировать только те способы, которые по стандарту работать не обязаны, а на то, что по стандарту — самый честный способ — вообще забить.
Нет такой подлости и мерзости, на которую бы не пошёл gcc ради бессмысленных 5% скорости в никому не нужном синтетическом тесте
Здравствуйте, T4r4sB, Вы писали:
TB>Нет, нормальные компиляторы нормально это воспринимают, а вот хвалёный ИЦЦ начинает нести бред при виде меммува, это да. Это гениально, оптимизировать только те способы, которые по стандарту работать не обязаны, а на то, что по стандарту — самый честный способ — вообще забить.
Стоит знать не только стандарт, но и причины заложенных в него ограничений и популярные реализации...
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
И пусть себе там компилятор разбирается, как оптимизировать?
E>MSVC 2015 Update 2, похоже, не разобрался, а просто дубово выполняет битовые операции.
E>
и and с чем-то не тем...
Тут или какой-то хитрый рюх или 1<<32 не в то посчитался, (лучше, конечно, 1ULL<<32 писать) или ещё чего...
Если таки это эквивалентный код, во что трудно поверить, то может он и быстрее, кстати...
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Здравствуйте, Erop, Вы писали:
E>А где тут минус E>и and с чем-то не тем... E>Тут или какой-то хитрый рюх или 1<<32 не в то посчитался, (лучше, конечно, 1ULL<<32 писать) или ещё чего...
Наверно, потому что имелось в виду
a + b — ( (1ull<<32)&((a+b)^~(a^b)) )
(В оригинале суффикса ull не было.)
Теперь получается
mov rax, QWORD PTR [rcx]
mov r8, rax
xor r8, rdx
not r8
lea rcx, QWORD PTR [rax+rdx]
xor r8, rcx
mov rcx, 4294967296 ; 0000000100000000H
and r8, rcx
sub rax, r8
add rax, rdx
ret 0
Здравствуйте, T4r4sB, Вы писали:
TB>memmove позволяет битово копировать данные разных типов без УБЭ. Но от огребания от индейца не спасёт, как уже сказали.
Похоже, memcpy/memmove самый годный способ. Clang, GCC и MSVC его оптимизируют на mov. Хотя, окончательное решение, что использовать, буду принимать на основе бенчмарков.
В проекте Chromium даже есть функция bit_cast, которая именно так и сделана.
Здравствуйте, T4r4sB, Вы писали:
TB>Интересное кино, не думал о таком. Но проверять бы не рискнул, т.к. стандарт до запятых не знаю, если что и знаю, то только в целом и общем.
Я неправ, так как там написано:
For any trivially copyable type T, if two pointers to T point to distinct T objects obj1 and obj2
А у меня объекты-то не различные, так что нет, так нельзя, видимо.
Здравствуйте, Eeel, Вы писали:
E>Здравствуйте, Erop, Вы писали:
E>>А где тут минус E>>и and с чем-то не тем... E>>Тут или какой-то хитрый рюх или 1<<32 не в то посчитался, (лучше, конечно, 1ULL<<32 писать) или ещё чего...
E>(В оригинале суффикса ull не было.)
Ясен пень, что не было, это же был псевдокод, всё-таки...
E>Но, что интересно, Clang 3.8 заменяет два независимых сложения на нечто подобное с битовыми операциями:
E>
E> movabs rax, -4294967296
E> mov rcx, rdi
E> and rcx, rax
E> add edi, esi
E> lea rcx, [rcx + rsi]
E> and rcx, rax
E> or rdi, rcx
E> mov rax, rdi
E> ret
E>
Ну так же предупреждал, что так, скорее всего, быстрее на актуальный x86
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Здравствуйте, Eeel, Вы писали:
E>По сути, это union. Но, насколько я понимаю, через union по стандарту C++ этого делать нельзя, так как это нарушение strict aliasing и UB (записывать один тип, а читать другой, несовместимый, тип).
Объясните мне, пожалуйста, а для чего ещё union, в таком случае? И как же раньше жили с LARGE_INTEGER?
Здравствуйте, placement_new, Вы писали:
_>Здравствуйте, Eeel, Вы писали:
E>>По сути, это union. Но, насколько я понимаю, через union по стандарту C++ этого делать нельзя, так как это нарушение strict aliasing и UB (записывать один тип, а читать другой, несовместимый, тип).
_>Strictly speaking, reading a member of a union different from the one written to is undefined in ANSI/ISO C99 except in the special case of type-punning to a char*, similar to the example below: Casting to char*. However, it is an extremely common idiom and is well-supported by all major compilers. As a practical matter, reading and writing to any member of a union, in any order, is acceptable practice.
_>http://cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html
А вот тут один интересный момент. В стандарте не говорится что это UB. Более того, там есть сноска в которой утверждается что в подобной ситуации происходит реинтерпретация данного участка памяти. По идее с точки зрения strict-aliasing здесь тоже всё должно быть нормально.
6.5.2.3 Structure and union members
95) If the member used to read the contents of a union object is not the same as the member last used to
store a value in the object, the appropriate part of the object representation of the value is reinterpreted
as an object representation in the new type as described in 6.2.6 (a process sometimes called ‘‘type
punning’’). This might be a trap representation.
Прошу поправить если я не прав.
PS. да, но всё равно union так лучше не использовать по многим причинам. Как вариант решения проблемы — memcpy или memmove.
Здравствуйте, alexanius, Вы писали: A>А вот тут один интересный момент. В стандарте не говорится что это UB. Более того, там есть сноска в которой утверждается что в подобной ситуации происходит реинтерпретация данного участка памяти. По идее с точки зрения strict-aliasing здесь тоже всё должно быть нормально. A> A>6.5.2.3 Structure and union members A>...
Это секция стандарта C99 ("чистого си"), а не C++. Насколько я понимаю, в C++ этот момент отличается (что плохо).
В C++ там есть только оговорка насчет структур с одинаковым префиксом.
C++ 14 9.5.1
In a union, at most one of the non-static data members can be active at any time, that is, the value of at
most one of the non-static data members can be stored in a union at any time. [ Note: One special guarantee
is made in order to simplify the use of unions: If a standard-layout union contains several standard-layout
structs that share a common initial sequence (9.2), and if an object of this standard-layout union type
contains one of the standard-layout structs, it is permitted to inspect the common initial sequence of any of
standard-layout struct members; see 9.2. — end note ]...
struct A
{
int a;
int b;
double c;
};
struct B
{
int a;
int b;
float c;
};
union Foo
{
A a;
B b;
};
Foo foo;
Здесь допустимо записать foo.a.a и foo.a.b, и прочитать, соответственно, foo.b.a и foo.b.b.
Здравствуйте, Eeel, Вы писали:
E>Здравствуйте, alexanius, Вы писали:
A>>А вот тут один интересный момент. В стандарте не говорится что это UB. Более того, там есть сноска в которой утверждается что в подобной ситуации происходит реинтерпретация данного участка памяти. По идее с точки зрения strict-aliasing здесь тоже всё должно быть нормально.
A>>
A>>6.5.2.3 Structure and union members
A>>...
E>Это секция стандарта C99 ("чистого си"), а не C++. Насколько я понимаю, в C++ этот момент отличается (что плохо).
Это 9899:201x, т.е. черновик C11. Но я даже могу более интересную цитаты из старого черновика привести. Вот, например, цитата из 9899:TC2 (черновик C99)
6.5.2.3 Structure and union members
EXAMPLE 3
The following is a valid fragment:
union {
struct {
int alltypes;
} n;
struct {
int type;
int intnode;
} ni;
struct {
int type;
double doublenode;
} nf;
} u;
u.nf.type = 1;
u.nf.doublenode = 3.14;
/* ... */
if (u.n.alltypes == 1)
if (sin(u.nf.doublenode) == 0.0)
/* ... */
Вообще это действительно один из очень неприятных моментов стандарта, который прописан довольно туманно, и с которым постоянно возникают проблемы.
Здравствуйте, alexanius, Вы писали:
A>Здравствуйте, Eeel, Вы писали:
E>>Здравствуйте, alexanius, Вы писали:
A>>>А вот тут один интересный момент. В стандарте не говорится что это UB. Более того, там есть сноска в которой утверждается что в подобной ситуации происходит реинтерпретация данного участка памяти. По идее с точки зрения strict-aliasing здесь тоже всё должно быть нормально.
A>>>
A>>>6.5.2.3 Structure and union members
A>>>...
E>>Это секция стандарта C99 ("чистого си"), а не C++. Насколько я понимаю, в C++ этот момент отличается (что плохо).
A>Это 9899:201x, т.е. черновик C11. Но я даже могу более интересную цитаты из старого черновика привести. Вот, например, цитата из 9899:TC2 (черновик C99)
Извиняюсь, ступил и не увидел что ТС про C++ говорит.
V>тут такая же проблема с выравниванием, или, из-за того что типы одинаковые, гарантий больше ?
По идее, гарантий никаких. В массиве данные идут подряд (чтобы работала арифметика указателей через sizeof), а в структуре может быть паддинг на 8 байт.
P.S. Товарищи минусующие, не стесняйтесь оставлять свою точку зрения! Ссылки на Стандарт караются плюсом в карму
Здравствуйте, Mr.Delphist, Вы писали:
MD>По идее, гарантий никаких. В массиве данные идут подряд (чтобы работала арифметика указателей через sizeof), а в структуре может быть паддинг на 8 байт.
кстати, интересное наблюдение
какие есть гарантии, что value32Hi ляжет точно в value[1] ?
Здравствуйте, uzhas, Вы писали:
U>Здравствуйте, Mr.Delphist, Вы писали:
MD>>По идее, гарантий никаких. В массиве данные идут подряд (чтобы работала арифметика указателей через sizeof), а в структуре может быть паддинг на 8 байт.
U>кстати, интересное наблюдение U>какие есть гарантии, что value32Hi ляжет точно в value[1] ?
Предлагаю посмотреть на проблему «массив vs. структура» с этой стороны:
if a is an expression of type cv std::complex<T>* and the expression a[i] is well-defined for an integer expression i, then:
— reinterpret_cast<cv T*>(a)[2*i] shall designate the real part of a[i], and
— reinterpret_cast<cv T*>(a)[2*i + 1] shall designate the imaginary part of a[i].
И попробовать придумать реализацию, где одновременно и std::complex<uint32_t> работает, и value32Hi не попадает в value[1].