В шейдере мы можем написать vec3 v0 = v1.xxy * 2, а также любую другую комбинацию x, y, z и w в зависимости от длины вектора. Я рассматриваю только размеры вектора до 4, как самые распространенные для использования. Полученный вектор может иметь не только ту же самую размерность, но и меньшую или большую, причем его компоненты могут быть скопированы в произвольном порядке. Эта операция называется 'swizzle', и она чертовски удобна для различных операций с малоразмерными векторами, особенно если они представляют игровые сущности в виде позиций, размера или цветов. Вектора используются повсюду в игровых проектах (да и не только в игровых), а не только в шейдерах. В какой-то момент было решено добавить 'swizzle' в наш игровой движок в базовые классы vec2, vec3 и vec4. Возникли вопросы: как добиться такого же синтаксического и семантического поведения в C++ коде, при этом минимизируя потери производительности.


Что в свизлинге тебе моем...

Лучше всего понять как работает swizzle - это показать на примерах:

vec3 vec{1.0, 2.0, 3.0};
vec4 a;
vec3 b;
vec2 c;
float d;

b = vec.xyz;  // b is now (1.0, 2.0, 3.0)
d = vec[2];   // d is now 3.0
a = vec.xxxx; // a is now (1.0, 1.0, 1.0, 1.0)
c = vec.zx;   // c is now (3.0, 1.0)
b = vec.rgb;  // b is now (1.0, 2.0, 3.0)
a = vec.yy;   // error; incostistent size

Есть несколько вариантов решения: использовать код из библиотеки glm, СxxSwizzle или написать свою реализацию. Библиотеку glm советую всем, кто начинает писать свой или хочет разобраться с игровыми движками. Однако, код в ней очень специфичный и многословный на мой взгляд, он является наследием трудных 90-х (первый код был написан в июне 1992 года, это больше 31 года назад).

Еще можно посмотреть реализацию здесь. Там реализовано много дополнительных функций, но в игровом движке обычно уже присутствует большая часть логики, хоть и в другом виде. Это решение хорошее, но, опять же, код там очень многословный. Мы же хотели получить простое и красивое решение, которое можно записать в пяти строчках кода.

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

  1. Скорость выполнения, решение не должно вносить вычислительной нагрузки по сравнению с эквивалентными скалярными операциями. Решение не должно жертвовать вычислительной эффективностью ради реализации свизлинга, по возможности сделать на интринсиках SSE

  2. Использование несмежных компонентов вектора, например, v0.xxy + v1.xzy. Это требует возможности доступа и выполнения операций над компонентами вектора в пользовательском порядке.

  3. Запись в измененный вектор с ограничениями: (от этого потом отказались, чтобы не раздувать логику, в 99% этим не пользуются) например v1.yxwy = v0; с условием, что повторные элементы запрещены явно, то есть каждому элементу должно быть присвоено значение только один раз, и ни один элемент не должен получить несколько присваиваний.

  4. Отсутствие дополнительных накладных расходов по памяти: базовый тип не должен измениться в размерах. Это означает, что vec3 будет занимать пространство, эквивалентное хранению трех элементов своего типа данных без дополнительных накладных расходов.

Есть несколько различных способов добиться синтаксиса v1.yxwz = v0; без скобок: макросы, использование union и прокси объекты. В случае с макросами можно скрыть функции, например:

#define yxwz _yxwz()
vec4 vecb = veca.yxwz

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

Так что, после обсуждения в курилке, остановились на реализации через union - псевдокод такого union'a будет выглядеть так:

union {
    float v[2];
    ??? xx;
    ??? xy;
    ??? yx;
    ??? yy;
    ...
};

Чтобы не писать много оберток и уложить новые типы данных в union, они должны быть plain only data, так проще будет применить фокус со свизлингом. Осталось сделать такую реализацию, чтобы не пришлось писать эти классы руками - пусть трудится компилятор. Выглядеть это будет как-то так (псевдокод):

template<T, X, Y>
struct proxy {
    template<T, X2, Y2>
    proxy operator = (const SwizzleProxy2<T, X2, Y2> &o) {
        ((T*)this)[X] = ((T*)&o)[X2];
        ((T*)this)[Y] = ((T*)&o)[Y2];
        return this;
    }
};

Тут мы ссылаемся на this т.е. начало класса, подразумевая, что как самостоятельный тип данных swizzling существовать не будет, а будет только отображаться на память базового класса. Попытка сделать реализацию в лоб, как выяснилось, вызывает приступы ворнингов почти у всех компиляторов и false positive срабатывания статических анализаторов. Но псевдокод выше демонстрирует основную идею реализации операторов для свизлинга с использованием двух элементов: xx, xy, wx и так далее. Аргументы шаблона X и Y могут быть любыми индексами элементов вектора.

Если вы заметили, то собственных данных у класса нет, а такое вольное обращение с указателем на начало класса вызывало не только кучу ворнингов при компиляции первых вариантов под ps5 сlang, но и увеличивало время компиляции почти на 10%. С условных 5 до шести минут. Но если задать, хотя бы минимальный объект в классе, то время компиляции возвращалось в норму.

template<T, int X, int Y>
struct proxy {
	byte v;
    ...
};

В итоге в класс был добавлен локальный массив, совпадающий по размерам с базовым. Так как все основные операторы уже были реализованы в классах vec2/3/4, то на долю swizzle класса остается только оператор приведения типа к вектору и базовая логика. Итак первая реализация, как она ушла в движок.

template<template<typename> class TT, typename T, size_t X, size_t Y>
struct swizzle_vec2{
  	T v[2];
	inline TT<T>& operator=	(const TT<T>& rhs) {
		v[X]				= rhs[0];					
		v[Y]				= rhs[1];					
		return				*(TT<T>*)this;
	}
	inline auto &operator=	(const swizzle_vec2& rhs) {
		v[X]				= rhs.v[X];
		v[Y]				= rhs.v[Y];
		return				*this;
	}
	inline operator TT<T>	() const { return TT<T>{v[X], v[Y]}; }
};

И копипаста для каждого из классов vec3/vec4. В движке придерживаются принципа DRY, но для обкатки фичи и быстрого запуска, решили улучшайзинг отложить на попозже, собрав отзывы о работе с новым функционалом.

template<template<typename> class TT, typename T, size_t X, size_t Y, size_t Z>
struct swizzle_vec3{
	T v[3];
	inline TT<T>& operator=	(const TT<T>& rhs) {
		v[X]				= rhs[0];					
		v[Y]				= rhs[1];					
		v[Z]				= rhs[2];					
		return				*(TT<T>*)this;
	}
  ...

	inline operator TT<T>	() const { return TT<T>{v[X], v[Y], v[Z]}; } 	// unpack
};

Чтобы свизлинг заработал прозрачно в коде, в каждый класс надо было еще добавить union-структуры, которые бы это обеспечивали:

// v2 swizzles
#define swizzle_v2      \
swizzle_vec2<T, 2, 0, 0> xx; \
swizzle_vec2<T, 2, 0, 1> xy; \
swizzle_vec2<T, 2, 1, 0> yx; \
swizzle_vec2<T, 2, 1, 1> yy; \
...

template <class T>
struct vec2 {
	union {
		struct { T		x, y; };
		swizzle_v2
	};
};
Финальный вариант
template<template<typename> class TT, typename T, size_t X, size_t Y>
struct swizzle_vec2 {
	T v[2];
	inline TT<T>& operator=	(const TT<T>& rhs) {
		v[X]				= rhs[0];					// access pack element 0
		v[Y]				= rhs[1];					// access pack element 1
		return				*(TT<T>*)this;
	}
	inline auto &operator=	(const swizzle_vec2& rhs) {
		v[X]				= rhs.v[X];
		v[Y]				= rhs.v[Y];
		return				*this;
	}

	inline operator TT<T>	() const { return TT<T>{v[X], v[Y]}; } 	// unpack
};

#define swizzle_v2(TT)      \
swizzle_vec2<TT, T, 0, 0> xx; \
swizzle_vec2<TT, T, 0, 1> xy; \
swizzle_vec2<TT, T, 1, 0> yx; \
swizzle_vec2<TT, T, 1, 1> yy;

template <class T>
struct vec2 {
	union {
		struct { T		x, y; };
        swizzle_v2(vec2)
	};
};

int main() {
    vec2<float> veca{0, 1};
    printf("veca{%f, %f}\n", veca.x, veca.y);

    vec2<float> vecb = veca.yx;
    printf("vecb{%f, %f}", vecb.x, vecb.y);
    return 0;
}

Попробовать можно тут.

Реализация для vec3/vec4 не отличалась сильно ничем, кроме большого макроса финальных swizzle-стуктур, но такова была цена - захотели упростить жизнь коллегам, значит больше логики в коде.

#define swizzle_v4      \
Swizzle_vec2<TT, T, 0, 0> xx; \
Swizzle_vec2<TT, T, 0, 1> xy; \
Swizzle_vec2<TT, T, 0, 2> xz; \
Swizzle_vec2<TT, T, 0, 3> xw; \
....
Swizzle_vec3<TT, T, 0, 0, 0> xxx; \
Swizzle_vec3<TT, T, 0, 0, 1> xxy; \
Swizzle_vec3<TT, T, 0, 0, 2> xxz; \
Swizzle_vec3<TT, T, 0, 0, 3> xxw; \
Swizzle_vec3<TT, T, 0, 1, 0> xyx; \
.....
Swizzle<TT, T, 0, 0, 0, 0> xxxx; \
Swizzle<TT, T, 0, 0, 0, 1> xxxy; \
Swizzle<TT, T, 0, 0, 0, 2> xxxz; \
Swizzle<TT, T, 0, 0, 0, 3> xxxw; \
Swizzle<TT, T, 0, 0, 1, 0> xxyx; \

Копипаста очень плохое решение в любом случае, но позволило выпустить этот функционал быстро и собрать фидбек. Решение работало, но знание что можно сделать лучше и меньшим количеством кода, и убрать копипасту не давало спокойно спать. Позже, на рефакторе было сделано общее решение и убраны простыни макросов.

Кот копипасту не одобряет!
Кот копипасту не одобряет!

Утро добрым не бывает

Однажды утром стали падать тесты. Падать стало вот на таком моменте:

    ...
    vec3<float> vecc{1, 2, 3};
    printf("vecc{%f, %f, %f}\n", vecc.x, vecc.y, vecc.z);

    vec3<float> vecd{0, 0, 0};
    vecd.xz = vecc.xz;
    printf("vecd{%f, %f, %f}\n", vecd.x, vecd.y, vecd.z); <<<<<<<
    ...

output:
	vecc{1.000000, 2.000000, 3.000000}
	vecd{1.000000, 2.000000, 0.000000} -> vecd должен быть {1.0, 0.0, 2.0}

Блейм по исходникам привел вот к такому изменению в коде:

template<template<typename> class TT, typename T, size_t X, size_t Y>
struct swizzle_vec2{
	T v[2];
	inline TT<T>& operator=	(const TT<T>& rhs) {
		v[X]				= rhs[0];					
		v[Y]				= rhs[1];					
		return				*(TT<T>*)this;
	}
  
	!!!! inline auto &operator=	(const swizzle_vec2& rhs) = default; !!!!

    inline operator TT<T>	() const { return TT<T>{v[X], v[Y]}; } 	
};
```

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

  inline auto &operator=	(const swizzle_vec2& rhs) = default;
  ->
  call    memset@PLTS						// vecd.xz = vecc.xz
  mov     rax, qword ptr [rbp - 36]			// vecd.xz = vecc.xz
  mov     qword ptr [rbp - 48], rax			// vecd.xz = vecc.xz

Ошибку быстро поправили, заодно выделили время на рефактор кода. В итоге все классы получилось свести к одному шаблонному:

template <typename T, int RSIZE, int... INDEXES>
struct swizzle {
	T v[RSIZE];
	static constexpr int indexes[] = {INDEXES...};

    template <int... INDEXES2>
    inline auto &operator=(const swizzle<T, RSIZE, INDEXES2...> &rhs) {
		static_assert(sizeof...(INDEXES) == RSIZE, "error: assigning swizzle of different dimensions");
		constexpr int rindexes[] = {INDEXES2...};
		for(int i = 0; i < sizeof...(INDEXES); ++i) { v[indexes + i]] = rhs.v[rindexes[i]]; }
		return *this;
	}

	inline auto &operator=(const swizzle &rhs) {
		for(int i = 0; i < sizeof...(INDEXES); ++i) { v[indexes[i]]	= rhs.v[indexes[i]]; }
		return *this;
	}
};

А для конкретных реализация под вектора осталось только операция приведения к конкретному типу:

template<class T> struct vec2;
template<class T> struct vec3;

template <typename T, int RSIZE, int... SWIZZLES>
struct swizzle2	: public swizzle<T, RSIZE, SWIZZLES...> {
    inline auto &operator=	(const vec2<T>& l) {
		static_assert		(sizeof...(SWIZZLES) == 2, "error: assigning swizzle2 not from vec2f");
		v[indexes[0]] = l.x;
		v[indexes[1]] = l.y;
		return				*this;
	}
	inline operator vec2<T> () const {
		static_assert(sizeof...(SWIZZLES) > 1, "error: no data that convert to vec2");
		return vec2<T>{v[indexes[0]], v[indexes[1]]};
	}
};

template <typename T, int RSIZE, int... SWIZZLES>
struct swizzle3 : public swizzle<T, RSIZE, SWIZZLES...> {
    inline auto &operator=	(const vec3<T>& l) {
		static_assert		(sizeof...(SWIZZLES) == 3, "error: assigning swizzle2 not from vec2f");
		v[indexes[0]] = l.x;
		v[indexes[1]] = l.y;
        v[indexes[2]] = l.z;
		return				*this;
	}
	inline operator vec3<T> () const {
		static_assert(sizeof...(SWIZZLES) > 2, "error: no data that convert to vec3");
		return vec3<T>{v[indexes[0]], v[indexes[1]], v[indexes[2]]};
	}
};

Для полного счастья остается свернуть простыню прокси-структур, которая все равно присутствует в каждом классе vec2/3/4

#define swizzle_v2      \
swizzle2<T, 2, 0, 0> xx; \
swizzle2<T, 2, 0, 1> xy; \
swizzle2<T, 2, 1, 0> yx; \
swizzle2<T, 2, 1, 1> yy; \
swizzle3<T, 3, 0, 0, 0> xxx; \
swizzle3<T, 3, 0, 0, 1> xxy; \
swizzle3<T, 3, 0, 1, 0> xyx; \
swizzle3<T, 3, 0, 1, 1> xyy; \
swizzle3<T, 3, 1, 0, 0> yxx; \
swizzle3<T, 3, 1, 0, 1> yxy; \
swizzle3<T, 3, 1, 1, 0> yyx; \
swizzle3<T, 3, 1, 1, 1> yyy; \
swizzle4<T, 4, 0, 0, 0, 0> xxxx; \
swizzle4<T, 4, 0, 0, 0, 1> xxxy; \
swizzle4<T, 4, 0, 0, 1, 0> xxyx; \
swizzle4<T, 4, 0, 0, 1, 1> xxyy; \
swizzle4<T, 4, 0, 1, 0, 0> xyxx; \
swizzle4<T, 4, 0, 1, 0, 1> xyxy; \
swizzle4<T, 4, 0, 1, 1, 0> xyyx; \
swizzle4<T, 4, 0, 1, 1, 1> xyyy; \
swizzle4<T, 4, 1, 0, 0, 0> yxxx; \
swizzle4<T, 4, 1, 0, 0, 1> yxxy; \
swizzle4<T, 4, 1, 0, 1, 0> yxyx; \
swizzle4<T, 4, 1, 0, 1, 1> yxyy; \
swizzle4<T, 4, 1, 1, 0, 0> yyxx; \
swizzle4<T, 4, 1, 1, 0, 1> yyxy; \
swizzle4<T, 4, 1, 1, 1, 0> yyyx; \
swizzle4<T, 4, 1, 1, 1, 1> yyyy;
...

К сожалению убрать макросы без новых макросов не удалось, поэтому приходится пользоваться темной стороны силы.

Используй макросы, Люк!
Используй макросы, Люк!

Если посмотреть на приведенный код, то можно заметить что получаемые значения имеют ограниченный набор входных данных x, y, z, w, более того - каждое положение внутри набора соответствуют конкретному состоянию класса свизлинга

xx -> swizzle2<T, 2, 0, 0> -> {0, 0}
xy -> swizzle2<T, 2, 0, 1> -> {0, 1}
yx -> swizzle2<T, 2, 1, 0> -> {1, 0}
...

Эта зависимость может быть выражена через constexpr функцию, например так

constexpr int inline swizzle_idx__(pcstr x, int offset) { 
  switch(*(x+offset)) { case 'x': return 0; case 'y': return 1;
                        case 'z': return 2;  case 'w': return 3; } 
  return -1; 
}

swizzle2<T, 2, 0, 0> xx 
-> swizzle2<T, 2, swizzle_idx__("xx", 0), swizzle_idx__("xx", 0)>

Пробуем свернуть это выражение в макрос

swizzle2<T, 2, 0, 0> xx; 
swizzle2<T, 2, 0, 1> xy; 
swizzle2<T, 2, 1, 0> yx; 
swizzle2<T, 2, 1, 1> yy; 

->
  
#define $(name) swizzle2<T, 2, swizzle_idx__(#name, 0), swizzle_idx__(#name, 1)> name;

$(xx) $(xy) $(yx) $(yy)

С vec3 и vec4 не должно возникнуть сложностей, принцип похожий - определяем макрос для свизлинга в конкретную последовательность:

#define $(name) swizzle3<T, 3, 
                        swizzle_idx__(#name, 0),
                        swizzle_idx__(#name, 1),
                        swizzle_idx__(#name, 2)> name;

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

#define _(x) $(x ## xx) $(x ## xy) $(x ## xz) $(x ## yx) $(x ## yy) $(x ## yz) $(x ## zx) $(x ## zy) $(x ## zz)
		_(x) _(y) _(z) _(w)

Финальный код классов векторов после этого будет выглядеть вот так:

template <class T>
struct vec2 {
	union {
		  struct { T x, y; };
		  #define $(name) swizzle2<T, 2, swizzle_idx__(#name, 0), swizzle_idx__(#name, 1)> name;
      		$(xx) $(xy) $(yx) $(yy)
          #undef $
	};
};

Код примера положу под спойлер, он не очень интересен, просто рабочая реализация (https://godbolt.org/z/Pnq9654bW)

Пример
template <typename T, int SIZE, int... INDEXES>
struct swizzle {
	T v[SIZE];
	static constexpr int indexes[] = {INDEXES...};

	template <int RSIZE, int... INDEXES2>
    inline auto &operator=	(const swizzle<T, RSIZE, INDEXES2...>& rhs) {
		static_assert		(SIZE == RSIZE, "error: assigning swizzle of different dimensions");
		constexpr int rindexes[] = {INDEXES2...};

		for(int i = 0; i < SIZE; ++i) {
			v[indexes[i]]	= rhs.v[rindexes[i]];
		}

		return				*this;
	}

	inline auto &operator=	(const swizzle& rhs) {
		for(int i = 0; i < SIZE; ++i) {
			v[indexes[i]]	= rhs.v[indexes[i]];
		}
		return				*this;
	}
};

template<class T> struct vec2;
template<class T> struct vec3;

template <typename T, int... SWIZZLES>
struct swizzle2	: public swizzle<T, SWIZZLES...> {
    inline swizzle<T, SWIZZLES...> &operator=	(const vec2<T>& l) {
		static_assert		(sizeof...(SWIZZLES) > 1, "error: assigning swizzle2 not from vec2f");
		this->v[this->indexes[0]]	= l.x;
		this->v[this->indexes[1]]	= l.y;
		return				*this;
	}

	inline operator vec2<T> () const {
		static_assert(sizeof...(SWIZZLES) > 1, "error: no data that convert to vec2");
		return vec2<T>{this->v[this->indexes[0]], this->v[this->indexes[1]]};
	}
};

template <typename T, int SIZE, int... SWIZZLES>
struct swizzle3				: public swizzle<T, SIZE, SWIZZLES...> {
	inline swizzle<T, SIZE, SWIZZLES...> &operator=	(const vec3<T>& l) {
		static_assert		(SIZE == 3, "error: assigning swizzle3 not from vec3f");
		this->v[this->indexes[0]] = l.x;
		this->v[this->indexes[1]] = l.y;
		this->v[this->indexes[2]] = l.z;
		return				*this;
	}

	inline operator vec3<T> () const {
		static_assert		(SIZE > 2, "error: no data that convert to vec3");
		return				vec3<T>{this->v[this->indexes[0]], this->v[this->indexes[1]], this->v[this->indexes[2]]};
	}
};

constexpr int inline swizzle_idx__(const char *x, int offset) { switch(*(x+offset)) { case 'x': return 0; case 'y': return 1; case 'z': return 2;  case 'w': return 3; } return -1; }

template <class T>
struct vec2 {
	union {
		  struct { T		x, y; };
		  #define $(name) swizzle2<T, 2, swizzle_idx__(#name, 0), swizzle_idx__(#name, 1)> name;
      		$(xx) $(xy) $(yx) $(yy)
          #undef $
	};
}; 

template <class T>
struct vec3 {
	union {
		struct { T		x, y, z; };

        #define $(name) swizzle2<T, 2, swizzle_idx__(#name, 0), swizzle_idx__(#name, 1)> name;
      		$(xx) $(xy) $(xz) $(yx) $(yy) $(yz) $(zx) $(zy) $(zz)
          #undef $

        #define $(name) swizzle3<T, 3, swizzle_idx__(#name, 0), swizzle_idx__(#name, 1),  swizzle_idx__(#name, 2)> name;
        #define _(x) $(x ## xx) $(x ## xy) $(x ## xz) $(x ## yx) $(x ## yy) $(x ## yz) $(x ## zx) $(x ## zy) $(x ## zz)
		    _(x) _(y) _(z) _(w)
        #undef  _
        #undef  $
	};
};

int main() {
    vec2<float> veca{0, 1};
    printf("veca{%f, %f}\n", veca.x, veca.y);

    vec2<float> vecb = veca.yx;
    printf("vecb{%f, %f}\n", vecb.x, vecb.y);

    vec3<float> vecc{1, 2, 3};
    printf("vecc{%f, %f, %f}\n", vecc.x, vecc.y, vecc.z);

    vec3<float> vecd{0, 0, 0};
    vecd.xz = vecc.xz;
    printf("vecd{%f, %f, %f}\n", vecd.x, vecd.y, vecd.z);
    return 0;
}

Бенчмарки

Если мы посмотрим, что делает наш свизлинг в асме, то делает он ровно то, что мы написали - перекидывает байты из одного места в другое (https://godbolt.org/z/9EPe3q4EE)

Для реализованного класса, разница между 2/3 свизлингом вполне ожидаема и зависит только от количества операций внутри.
https://quick-bench.com/q/MhxnpfYU9-wTWiXqlTiCONujBJA

К сожалению, веб версия не позволяет затащить код из glm и cxxswizzle, пришлось разворачивать локально. volatile здесь сделан, чтобы компилятор не смог намухлевать с присвоением констант.

float volatile xx;
float volatile x = xx + 1;
float volatile y = xx + 2;
float volatile z = xx + 3;

static void Vec3Swizzling(benchmark::State& state) {
  for (auto _ : state) {
    vec3<float> veca{x, y, z};
	vec3<float> vecb{1, 2, 3};
	vecb.xz = veca.xz;
    benchmark::DoNotOptimize(veca);
	benchmark::DoNotOptimize(vecb);
  }
}

BENCHMARK(Vec3Swizzling);

static void Vec3SwizzlingGlm(benchmark::State& state) {
  for (auto _ : state) {
    glm::vec3<float> vecc{x, y, z};
	glm::vec3<float> vecd{0, 0, 0};
	vecd.xz = vecc.xz;
	benchmark::DoNotOptimize(vecc);
	benchmark::DoNotOptimize(vecd);
  }
}
BENCHMARK(Vec3SwizzlingGlm);

static void Vec3SwizzlingCxxSwizzle(benchmark::State& state) {
  for (auto _ : state) {
    cxx::vec3<float> vecc{x, y, z};
	cxx::vec3<float> vecd{0, 0, 0};
	vecd.xz = vecc.xz;
	benchmark::DoNotOptimize(vecc);
	benchmark::DoNotOptimize(vecd);
  }
}
BENCHMARK(Vec3SwizzlingCxxSwizzle);

По бенчам текущая реализация быстрее реализации в glm в 1.2 раза и CxxSwizzle в два раза. Ускорение достигается за счет отсутствие лишних проверок, которые есть в этих либах, и лучшей адаптации к структурам данных движка.

Можно еще ускорить свизлинг, если воспользоваться интринсиками SSE для перемешивания, тогда в асме это будет вообще одна операция. Есть вопрос как это все ляжет в памяти, но это будет тема следующего рефатокторинга.

Комментарии (23)


  1. alexac
    10.12.2023 20:44

    Если использовать clang, то есть удобный экстеншн (https://clang.llvm.org/docs/LanguageExtensions.html#vectors-and-extended-vectors), который позволяет объявлять векторные типы с такой же семантикой, как в OpenCL. Именно этот экстеншн эппл использует, чтобы имлементировать векторные типы в simd фреймворке.

    Работает через typedef/using с аттрибутом ext_vector_type(N):

    #if __has_attribute(ext_vector_type)
    using float2 = float __attribute__((ext_vector_type(2)));
    using float3 = float __attribute__((ext_vector_type(3)));
    using float4 = float __attribute__((ext_vector_type(4)));
    
    using int2 = int __attribute__((ext_vector_type(2)));
    using int3 = int __attribute__((ext_vector_type(3)));
    using int4 = int __attribute__((ext_vector_type(4)));
    #endif
    

    Да, это не стандартное поведение а специфичный для компилятора экстеншн, но зато работает без магии на макросах, да и вовсе без какого-либо дополнительного кода. И транслируется в нативные simd инструкции автоматически.

    У gcc есть подобный экстеншн и аттрибут для объявления векторных типов, но swizzle там не поддерживается.


    1. dalerank Автор
      10.12.2023 20:44

      угу, есть такое, но под коробку тоже надо собирать, вообще есть идея все реализации переписать на sse, чтобы не зависеть от имплементации в компиляторе


  1. kovserg
    10.12.2023 20:44

    А чем такая запись

    b = vec.xyz;  // b is now (1.0, 2.0, 3.0)
    d = vec[2];   // d is now 3.0
    a = vec.xxxx; // a is now (1.0, 1.0, 1.0, 1.0)
    

    лучше чем такая?

    b = vec.compose(0,1,2);
    d = vec.compose(2);
    a = vec.compose(0,0,0,0);
    


    1. dalerank Автор
      10.12.2023 20:44

      читабельностью?


      1. kovserg
        10.12.2023 20:44

        Особенно когда есть xyzs, rgba, uvwt. То можно ругательства получать из последовательности символов. Но кроме эстетического удовлетворения никакой практической пользы не видно.
        Так тоже не читабельно?

        enum { R,G,B }; enum { X,Y,Z,S };
        b = vec.compose(R,G,B);
        d = vec.compose(Z);
        a = vec.compose(X,X,X,X);
        


        1. dalerank Автор
          10.12.2023 20:44

          ну ругательства можно получить даже из слова "счастье", если записать его буквами "a, п, о, ж"
          ориентировались всеже на шейдерный синтаксис, где .xyzw канонична
          a = vec.xxxx;


  1. Kelbon
    10.12.2023 20:44

    Признаюсь, не прочитал полностью, но вообще не понял как это планировалось делать через union, ведь даже игнорируя УБ, xxx из xyz реинтерпретацией байт мягко говоря сложно( не говоря уже о xyzxyz)

    Я бы сделал это consteval функцией, которая возвращает пак из указателей на филды(ну или просто индексы). Далее раскрываем пак складывая из вектора в vec<N> какой-то, ведь нужно любого размера. Выглядело бы это как-то так

    vec<5> x = swizzle<"xxxyy">(v);

    Но синтаксис уже какой хотите такой и добавляйте. В целом как-то не вижу где проблемы могут возникнуть в этой задаче(кроме того что вектора<N> может быть не предусмотрено и тогда придётся либо ограничиться массивом, когда размер != 2 или 3)

    P.S. делать макросы из 2/3/4 букв без префиксов это конечно сильно, в моём сценарии это были бы либо переменные, либо функции, вызывающие нужный swizzle


    1. dalerank Автор
      10.12.2023 20:44

      ну задачи получать неразмерные вектора вида xxxyy не стояло, потому что работать с ними не получится, а вот .xxyy из .xy вполне можно, компилятор это сводит в итоге к операциям с элементами массивов. Все это давно пройдено в компиляторах шейдеров


      1. KanuTaH
        10.12.2023 20:44

        Честно говоря, выглядит все это довольно хрупко. Тут целых два сомнительных места: во-первых, в C++ нельзя, записав в один элемент union, читать из другого (это UB), а во-вторых, структура и массив строго говоря не являются layout compatible, так что то, что это все сейчас работает - не более чем удачное совпадение. В компилятор добавят какие-нибудь новые оптимизации, как уже бывало, и все это поломается.


        1. dalerank Автор
          10.12.2023 20:44

          почему же, вполне себе работает, не только у нас но и у Unreal 4/5
          https://github.com/EpicGames/UnrealEngine/blob/072300df18a94f18077ca20a14224b5d99fee872/Engine/Source/Runtime/Core/Public/Math/Vector.h#L50

          template<typename T>
          struct TVector
          {
          	static_assert(std::is_floating_point_v<T>, "T must be floating point");
          
          public:
          	using FReal = T;
          
          	union   <<<<<<<<<<<<<<<<<<<< все это свернуто в юнион для удобства
          	{
            		struct                               <<<<<<<<<<  поля xyz
          		{
          			/** Vector's X component. */
          			T X;
          
          			/** Vector's Y component. */
          			T Y;
          
          			/** Vector's Z component. */
          			T Z;
          		};
          
          		UE_DEPRECATED(all, "For internal use only")
          		T XYZ[3];                              <<<<<<<<<<<<< массив
          	};


          1. KanuTaH
            10.12.2023 20:44

            почему же, вполне себе работает

            Еще раз: это UB. То, что это пока работает, вообще не показатель. Поработает-поработает, и перестанет. Вообще, на тему сделать структуры, которые были бы layout compatible с соответствующими массивами, есть пропозалы, например этот. Но на данный момент это все-таки UB.


            1. dalerank Автор
              10.12.2023 20:44

              тогда скорее уж узаконенная практикой вещь, потому что очень много кода работает вот с такой конвертацией, и это легальный способ представления float <-> int
              union {
              uint32_t i;
              float f;
              }
              v;
              и запрет его в новых версиях поломает кучу софта, на что вендоры пойдут в крайнем случае, скорее всего никогда


              1. KanuTaH
                10.12.2023 20:44

                и запрет его в новых версиях поломает кучу софта, на что вендоры пойдут в крайнем случае, скорее всего никогда

                Ну, может быть и так. А может быть и нет. В пользу последнего говорит например то что вместо "узаконивания" type punning через union в C++20 добавили std::bit_cast(). В общем, кто знает :)


                1. ImagineTables
                  10.12.2023 20:44

                  А что мешало просто перевести УБ в ДБ?


                  1. KanuTaH
                    10.12.2023 20:44

                    Ну, у меня тут есть только предположения, хотя и много разных. Например, вызов std::bit_cast обозначает начало времени жизни соответствующего объекта явно, а обращение через неактивный член union - нет (особенно если обращение происходит посредством ссылок/указателей через третьи руки). std::bit_cast осуществляет минимальный набор проверок - что размеры типов совпадают и что оба типа являются "тривиально копируемыми", т.е. по факту могут быть скопированы через вызов memcpy(), иначе код не скомпилируется. Например, нельзя просто взять и написать такое:

                    char c = 0;
                    int i = std::bit_cast<int>(c);

                    union не выполняет никаких проверок вообще, потому что у него другая задача, а именно - просто хранить несколько объектов разных типов в одном storage, но не "одновременно". Ему плевать, если ты напишешь так:

                    union {
                        char c;
                        int i;
                    } u;
                    
                    u.c = 0;
                    return u.i;

                    Можно и еще придумать причины.


  1. ya_ne_znau
    10.12.2023 20:44

    "Swizzle в vec2/vec3/vec4 внутри C++ как в OpenGL"

    Чем кодогенерация inline методов с возвратом векторов из соответсвующих полей не подошла?

    Что-то типа такого:

    class vec2 {
    public:
      int x;
      int y;
      // ctor, dtor, ...
      inline vec2 xx() { return vec2(x, x); }
      inline vec2 yy() { return vec2(y, y); }
      // ...
    }

    Не знаком с плюсами даже близко, так что поправьте если где-то ошибся


    1. dalerank Автор
      10.12.2023 20:44

      Это был как один из вариантов, если не получится с юнион, но хотелось все же нативный "шейдерный" синтаксис получить


      1. ya_ne_znau
        10.12.2023 20:44

        Так и не понял, как планировалось использовать union? Для свиззлинга ведь нужны различные места памяти в исходном объекте, а юнион такого не может делать. В Си ещё хоть какой-то type punning был, а в плюсах доступ к неактивному члену это точно UB, да ещё и делает что-то не то.

        Максимум, который мне представляется юнионом, это

        // C
        typedef union {
          struct {
            int x;
            int y;
          };
          int data[2];
        } vec2;
        
        vec2 a;
        a.x;
        a.y;
        a.data[0];
        a.data[1];

        Неужели в C++ с ними придумали ещё более опасные для созерцания вещи?


        1. dalerank Автор
          10.12.2023 20:44

          Так положите рядом ещё одну структуру, которая называется yx и перепишите ей оператор присвоения и приведения. Собственно это и будет решением для свизлинга


  1. voldemar_d
    10.12.2023 20:44

    Фраза непонятна:

    Встали вопросы чтобы добиться такого же синтаксического и семантического поведения


    1. dalerank Автор
      10.12.2023 20:44

      спасибо, поправил


  1. Readme
    10.12.2023 20:44

    просто рабочая реализация (https://godbolt.org/z/ff55c4YnP)

    Почему-то прямо с порога некорректный ответ (clang-trunk -O0):

    veca{0.000000, 1.000000}
    vecb{1076475645313255735296.000000, 1.000000}
    vecc{1.000000, 2.000000, 3.000000}
    vecd{1.000000, 0.000000, 3.000000}
    

    На -O1 и выше всё норм. UB таки стреляет?


    1. dalerank Автор
      10.12.2023 20:44

      Виноват, какуюто старую ссылку подпихнул (спасибо, поправил)
      https://godbolt.org/z/Pnq9654bW