Заранее отказ от ответственности: этот вопрос больше похож на вопрос языкового юриста или попытку определить границы возможностей компиляторов, поэтому, пожалуйста, не зацикливайтесь слишком на необходимости следующих требований.< /p>
Предположим, я хочу написать собственный класс vec3, который хранит три числа с плавающей запятой в виде массива (или вектора в математическом смысле) на C++, и я ожидаю, что он будет соответствовать следующим спецификациям:
Его отдельные члены должны быть доступны напрямую, как v.x, а не через средства доступа, такие как v.x(), поскольку я хочу использовать его взаимозаменяемо с, скажем, glm::vec3.
Он должен иметь оператор[], который возвращает соответствующий элемент в этом измерении, а возвращаемое значение должно иметь ссылочный тип float&, поскольку Я хочу использовать его в общем контексте, который принимает любой динамически индексируемый тип, подобный массиву.
Его оператор[] реализация должна быть максимально производительной, поскольку ее можно вызывать чертовски много раз каждую миллисекунду.
Это должно быть тривиально- копируемые, а x, y и z должны иметь последовательные адреса, так как я хочу напрямую скопировать массив vec3 в память графического процессора и использовать их в шейдере или чем-то еще.
Его реализация оператора[] не должна вызывать UB.
Я попробовал несколько реализаций, которые я мог бы подумать (https://godbolt.org/z/5fYWb8ET8), но ни один из них не соответствует всем приведенным выше спецификациям: наиболее производительные из них напрямую нарушают некоторые требования языка C++, а остальные не столь производительны. Идеальный сгенерированный код должен напрямую вычислять возвращаемый адрес, используя адресацию базового смещения (
vec3::idx0(unsigned long):
lea rax, [rdi + 4*rsi]
ret
vec3::idx1(unsigned long):
lea rax, [rdi + 4*rsi]
ret
Другие реализации, в том числе ручная диспетчеризация с использованием if, switch, индексация по абсолютным адресам и индексация по относительным адресам, все привели к гораздо менее эффективным ассемблерным кодам:
vec3::idx2(unsigned long):
mov rax, rdi
test rsi, rsi
je .LBB2_4
cmp rsi, 2
jne .LBB2_2
add rax, 8
.LBB2_4:
ret
.LBB2_2:
add rax, 4
ret
vec3::idx3(unsigned long):
mov rax, rdi
test rsi, rsi
je .LBB3_4
cmp rsi, 2
jne .LBB3_2
add rax, 8
.LBB3_4:
ret
.LBB3_2:
add rax, 4
ret
vec3::idx4(unsigned long):
mov qword ptr [rsp - 24], rdi
lea rax, [rdi + 4]
mov qword ptr [rsp - 16], rax
add rdi, 8
mov qword ptr [rsp - 8], rdi
mov rax, qword ptr [rsp + 8*rsi - 24]
ret
vec3::idx5(unsigned long):
mov rax, rdi
lea rcx, [rip + .L__const.vec3::idx5(unsigned long).accessors]
add rax, qword ptr [rcx + 8*rsi]
ret
.L__const.vec3::idx5(unsigned long).accessors:
.quad 0
.quad 4
.quad 8
Я также пытался предоставить компилятору дополнительную информацию (используя [[assume]] или встроенные функции), которая могла бы помочь ему проанализировать числовые отношения между i и возвращенный адрес, но ни один из них не сработал. Итак, вот настоящие вопросы:
Есть ли какой-нибудь способ проинструктировать компиляторы генерировать идеальный код, с реализацией, написанной на «стандартном» C++ без UB? Возможно, я просто не нашел правильный способ сообщить составителям дополнительную информацию.
Если нет, то это из-за ограничения языковых стандартов или это из-за отсутствия в компиляторах определенных методов оптимизации? В случае с idx5 компиляторы могли бы определить, что загруженное смещение всегда имеет то же значение, что и 4*i.
Есть ли какое-либо определенное изменение (или какое-либо существующее предложение) в стандарте C++, которое могло бы помочь в этой ситуации? Самое близкое, что я могу придумать, это следующее: решат ли они проблему?
Разрешение доступа неактивным членам профсоюза, чьи все члены представляют собой тривиально копируемые типы без дополнений.
Дождитесь реализации std::start_lifetime_as и используйте это для реализации оператора[] как idx6.
[b]Заранее отказ от ответственности: этот вопрос больше похож на вопрос языкового юриста или попытку определить границы возможностей компиляторов, поэтому, пожалуйста, не зацикливайтесь слишком на необходимости следующих требований.[/b]< /p> Предположим, я хочу написать собственный класс vec3, который хранит три числа с плавающей запятой в виде массива (или вектора в математическом смысле) на C++, и я ожидаю, что он будет соответствовать следующим спецификациям: [list] [*]Его отдельные члены должны быть доступны напрямую, как v.x, а не через средства доступа, такие как v.x(), поскольку я хочу использовать его взаимозаменяемо с, скажем, glm::vec3.
[*]Он должен иметь оператор[], который возвращает соответствующий элемент в этом измерении, а возвращаемое значение должно иметь ссылочный тип float&, поскольку Я хочу использовать его в общем контексте, который принимает любой динамически индексируемый тип, подобный массиву.
[*]Его оператор[] реализация должна быть максимально производительной, поскольку ее можно вызывать чертовски много раз каждую миллисекунду.
[*]Это должно быть тривиально- копируемые, а x, y и z должны иметь последовательные адреса, так как я хочу напрямую скопировать массив vec3 в память графического процессора и использовать их в шейдере или чем-то еще.
[*] Его реализация оператора[] не должна вызывать UB.
[/list] Я попробовал несколько реализаций, которые я мог бы подумать (https://godbolt.org/z/5fYWb8ET8), но ни один из них не соответствует всем приведенным выше спецификациям: наиболее производительные из них напрямую нарушают некоторые требования языка C++, а остальные не столь производительны. Идеальный сгенерированный код должен напрямую вычислять возвращаемый адрес, используя адресацию базового смещения ([code]lea[/code] на x64) с this в качестве базы и 4*i в качестве смещения, как и в случае с idx0 и idx1, но их реализация зашла в тупик. область UB: Реализация: [code]struct vec3 { union { struct { float x; float y; float z; }; float xyz[3]; };
[[gnu::used]] constexpr float& idx0(size_t i) { // UB as violating the strict aliasing rule return reinterpret_cast(this)[i]; }
[[gnu::used]] constexpr float& idx1(size_t i) { // The xyz variant is only used here // UB as accessing union from potentially non-active member return xyz[i]; } }; [/code] Сгенерирован asm на Clang: [code]vec3::idx0(unsigned long): lea rax, [rdi + 4*rsi] ret
vec3::idx1(unsigned long): lea rax, [rdi + 4*rsi] ret [/code] Другие реализации, в том числе ручная диспетчеризация с использованием if, switch, индексация по абсолютным адресам и индексация по относительным адресам, все привели к гораздо менее эффективным ассемблерным кодам: [code]struct vec3 { // same data members as above
[[gnu::used]] constexpr float& idx2(size_t i) { if (i == 0) { return x; } else if (i == 1) { return y; } else if (i == 2) { return z; } else { std::unreachable(); } }
[[gnu::used]] constexpr float& idx3(size_t i) { switch (i) { case 0: return x; case 1: return y; case 2: return z; default: std::unreachable(); } }
.L__const.vec3::idx5(unsigned long).accessors: .quad 0 .quad 4 .quad 8 [/code] Я также пытался предоставить компилятору дополнительную информацию (используя [[assume]] или встроенные функции), которая могла бы помочь ему проанализировать числовые отношения между i и возвращенный адрес, но ни один из них не сработал. Итак, вот настоящие вопросы:
[list] [*]Есть ли какой-нибудь способ проинструктировать компиляторы генерировать идеальный код, с реализацией, написанной на «стандартном» C++ без UB? Возможно, я просто не нашел правильный способ сообщить составителям дополнительную информацию.
[*]Если нет, то это из-за ограничения языковых стандартов или это из-за отсутствия в компиляторах определенных методов оптимизации? В случае с idx5 компиляторы могли бы определить, что загруженное смещение всегда имеет то же значение, что и 4*i. [*]Есть ли какое-либо определенное изменение (или какое-либо существующее предложение) в стандарте C++, которое могло бы помочь в этой ситуации? Самое близкое, что я могу придумать, это следующее: решат ли они проблему?
Разрешение доступа неактивным членам профсоюза, чьи все члены представляют собой тривиально копируемые типы без дополнений.
[*]Дождитесь реализации std::start_lifetime_as и используйте это для реализации оператора[] как idx6.
Заранее отказ от ответственности: этот вопрос больше похож на вопрос лингвиста или попытку определить границы возможностей компиляторов, поэтому, пожалуйста, не зацикливайтесь слишком на необходимости следующих требований.
Предположим, я хочу...