просто из любопытства я реализовал утилиты vector3 тремя способами: массив (с typedef), класс и структура
Это реализация массива:
typedef float newVector3[3];
namespace vec3{
void add(const newVector3& first, const newVector3& second, newVector3& out_newVector3);
void subtract(const newVector3& first, const newVector3& second, newVector3& out_newVector3);
void dot(const newVector3& first, const newVector3& second, float& out_result);
void cross(const newVector3& first, const newVector3& second, newVector3& out_newVector3);
}
// implementations, nothing fancy...really
void add(const newVector3& first, const newVector3& second, newVector3& out_newVector3)
{
out_newVector3[0] = first[0] + second[0];
out_newVector3[1] = first[1] + second[1];
out_newVector3[2] = first[2] + second[2];
}
void subtract(const newVector3& first, const newVector3& second, newVector3& out_newVector3){
out_newVector3[0] = first[0] - second[0];
out_newVector3[1] = first[1] - second[1];
out_newVector3[2] = first[2] - second[2];
}
void dot(const newVector3& first, const newVector3& second, float& out_result){
out_result = first[0]*second[0] + first[1]*second[1] + first[2]*second[2];
}
void cross(const newVector3& first, const newVector3& second, newVector3& out_newVector3){
out_newVector3[0] = first[0] * second[0];
out_newVector3[1] = first[1] * second[1];
out_newVector3[2] = first[2] * second[2];
}
}
И реализация класса:
class Vector3{
private:
float x;
float y;
float z;
public:
// constructors
Vector3(float new_x, float new_y, float new_z){
x = new_x;
y = new_y;
z = new_z;
}
Vector3(const Vector3& other){
if(&other != this){
this->x = other.x;
this->y = other.y;
this->z = other.z;
}
}
}
Конечно, он содержит другие функции, которые обычно появляются в классе Vector3.
И наконец, реализация структуры:
struct s_vector3{
float x;
float y;
float z;
// constructors
s_vector3(float new_x, float new_y, float new_z){
x = new_x;
y = new_y;
z = new_z;
}
s_vector3(const s_vector3& other){
if(&other != this){
this->x = other.x;
this->y = other.y;
this->z = other.z;
}
}
Опять же, я опустил некоторые другие общие функции Vector3.
Теперь я позволил всем трем из них создать 9000000 новых объектов и выполнить 9000000 раз для перекрестного продукта (я записал огромный кусок данных в кеш после завершения одного из них, чтобы избежать их кеша).
Вот тестовый код:
const int K_OPERATION_TIME = 9000000;
const size_t bigger_than_cachesize = 20 * 1024 * 1024;
void cleanCache()
{
// flush the cache
long *p = new long[bigger_than_cachesize];// 20 MB
for(int i = 0; i < bigger_than_cachesize; i++)
{
p[i] = rand();
}
}
int main(){
cleanCache();
// first, the Vector3 struct
std::clock_t start;
double duration;
start = std::clock();
for(int i = 0; i < K_OPERATION_TIME; ++i){
s_vector3 newVector3Struct = s_vector3(i,i,i);
newVector3Struct = s_vector3::cross(newVector3Struct, newVector3Struct);
}
duration = ( std::clock() - start ) / (double) CLOCKS_PER_SEC;
printf("The struct implementation of Vector3 takes %f seconds.\n", duration);
cleanCache();
// second, the Vector3 array implementation
start = std::clock();
for(int i = 0; i < K_OPERATION_TIME; ++i){
newVector3 newVector3Array = {i, i, i};
newVector3 opResult;
vec3::cross(newVector3Array, newVector3Array, opResult);
}
duration = ( std::clock() - start ) / (double) CLOCKS_PER_SEC;
printf("The array implementation of Vector3 takes %f seconds.\n", duration);
cleanCache();
// Third, the Vector3 class implementation
start = std::clock();
for(int i = 0; i < K_OPERATION_TIME; ++i){
Vector3 newVector3Class = Vector3(i,i,i);
newVector3Class = Vector3::cross(newVector3Class, newVector3Class);
}
duration = ( std::clock() - start ) / (double) CLOCKS_PER_SEC;
printf("The class implementation of Vector3 takes %f seconds.\n", duration);return 0;
}
Результат потрясающий.
struct
а также class
реализация завершает задачу за 0,23 секунды,
в то время как array
реализация занимает всего 0,08 секунды!
Если у массива есть существенное преимущество в производительности, как это, хотя его синтаксис был бы уродливым, во многих случаях его стоит использовать.
Так что я действительно хочу убедиться, должно ли это произойти? Спасибо!
Краткий ответ: это зависит. Как вы можете заметить, есть разница, если компилировать без оптимизации.
Когда я компилирую (все функции встроены) с оптимизацией на (-O2
или же -O3
) нет никакой разницы (читайте дальше, чтобы увидеть, что это не так просто, как кажется).
Optimization Times (struct vs. array)
-O0 0.27 vs. 0.12
-O1 0.14 vs. 0.04
-O2 0.00 vs. 0.00
-O3 0.00 vs. 0.00
Нет никакой гарантии, что оптимизация может / будет делать ваш компилятор, поэтому полный ответ «это зависит от вашего компилятора». Сначала я доверял бы своему компилятору делать правильные вещи, в противном случае я должен начать программирование на ассемблере. Только если эта часть кода представляет собой реальную бутылку, стоит подумать о помощи компилятору.
Если скомпилировано с -O2
твой код занимает точно 0.0
секунд для обеих версий, но это потому, что оптимизаторы видят, что эти значения не используются вообще, поэтому он просто отбрасывает весь код!
Давайте убедимся, что этого не произойдет:
#include <ctime>
#include <cstdio>
const int K_OPERATION_TIME = 1000000000;
int main(){
std::clock_t start;
double duration;
start = std::clock();
double checksum=0.0;
for(int i = 0; i < K_OPERATION_TIME; ++i){
s_vector3 newVector3Struct = s_vector3(i,i,i);
newVector3Struct = s_vector3::cross(newVector3Struct, newVector3Struct);
checksum+=newVector3Struct.x +newVector3Struct.y+newVector3Struct.z; // actually using the result of cross-product!
}
duration = ( std::clock() - start ) / (double) CLOCKS_PER_SEC;
printf("The struct implementation of Vector3 takes %f seconds.\n", duration);
// second, the Vector3 array implementation
start = std::clock();
for(int i = 0; i < K_OPERATION_TIME; ++i){
newVector3 newVector3Array = {i, i, i};
newVector3 opResult;
vec3::cross(newVector3Array, newVector3Array, opResult);
checksum+=opResult[0] +opResult[1]+opResult[2]; // actually using the result of cross-product!
}
duration = ( std::clock() - start ) / (double) CLOCKS_PER_SEC;
printf("The array implementation of Vector3 takes %f seconds.\n", duration);
printf("Checksum: %f\n", checksum);
}
Вы увидите следующие изменения:
1e9
итерации, чтобы получить значимые времена.С этим изменением мы видим следующие тайминги (компилятор Intel):
Optimization Times (struct vs. array)
-O0 33.2 vs. 17.1
-O1 19.1 vs. 7.8
-Os 19.2 vs. 7.9
-O2 0.7 vs. 0.7
-O3 0.7 vs. 0.7
Я немного разочарован, что -Os
имеет такую плохую производительность, но в противном случае вы можете видеть, что при оптимизации нет разницы между структурами и массивами!
Лично мне нравится -Os
много, потому что он производит сборку, я могу понять, поэтому давайте посмотрим, почему это так медленно.
Самое очевидное, не заглядывая в полученную сборку: s_vector3::cross
возвращает s_vector3
-объект, но мы присваиваем результат уже существующему объекту, поэтому, если оптимизатор не видит, что старый объект больше не используется, он может быть не в состоянии выполнить RVO. Так что давай заменим
newVector3Struct = s_vector3::cross(newVector3Struct, newVector3Struct);
checksum+=newVector3Struct.x +newVector3Struct.y+newVector3Struct.z;
с:
s_vector3 r = s_vector3::cross(newVector3Struct, newVector3Struct);
checksum+=r.x +r.y+r.z;
Там результаты сейчас: 2.14 (struct) vs. 7.9
— это настоящее улучшение!
Мой вывод: оптимизатор проделывает отличную работу, но мы можем немного помочь в случае необходимости.
В этом случае нет. Что касается процессора; классы, структуры и массивы являются просто макетами памяти, и макет в этом случае идентичен. В сборках без релиза, если вы используете встроенные методы, они может быть скомпилированы в реальные функции (прежде всего, чтобы помочь отладчику шагнуть в методы), так что может иметь небольшое влияние.
Дополнение на самом деле не хорошо способ проверить производительность типа Vec3. Точечный и / или перекрестный продукт обычно является лучшим способом тестирования.
Если вы действительно заботитесь о производительности, вы, в основном, захотите использовать подход со структурой массивов (вместо массива структур, как вы делали выше). Это позволяет компилятору применять автоматическую векторизацию.
т.е. вместо этого:
constexpr int N = 100000;
struct Vec3 {
float x, y, z;
};
inline float dot(Vec3 a, Vec3 b) { return a.x*b.x + a.y*b.y + a.z*b.z; }
void dotLots(float* dps, const Vec3 a[N], const Vec3 b[N])
{
for(int i = 0; i < N; ++i)
dps[i] = dot(a[i], b[i]);
}
Вы бы сделали это:
constexpr int N = 100000;
struct Vec3SOA {
float x[N], y[N], z[N];
};
void dotLotsSOA(float* dps, const Vec3SOA& a, const Vec3SOA& b)
{
for(int i = 0; i < N; ++i)
{
dps[i] = a.x[i]*b.x[i] + a.y[i]*b.y[i] + a.z[i]*b.z[i];
}
}
Если вы скомпилируете с -mavx2 и -mfma, то последняя версия будет довольно хорошо оптимизирована.