添加未使用的内存时性能下降
Performance drop when adding memory that is not used
当我偶然发现这种奇怪的性能下降时,我正在玩一个简单的“游戏”来测试面向数据设计的不同方面。
我有这个结构来存储游戏飞船的数据:
constexpr int MAX_ENEMY_SHIPS = 4000000;
struct Ships
{
int32_t count;
v2 pos[MAX_ENEMY_SHIPS];
ShipMovement movements[MAX_ENEMY_SHIPS];
ShipDrawing drawings[MAX_ENEMY_SHIPS];
//ShipOtherData other[MAX_ENEMY_SHIPS];
void Add(Ship ship)
{
pos[count] = ship.pos;
movements[count] = { ship.dir, ship.speed };
drawings[count] = { ship.size, ship.color };
//other[count] = { ship.a, ship.b, ship.c, ship.d };
count++;
}
};
然后我有更新运动数据的功能:
void MoveShips(v2* positions, ShipMovement* movements, int count)
{
ScopeBenchmark bench("Move Ships");
for(int i = 0; i < count; ++i)
{
positions[i] = positions[i] + (movements[i].dir * movements[i].speed);
}
}
我的理解是,由于 MoveShips 函数仅使用位置和移动数组,因此 Ships 结构中的额外内存不会影响其性能。但是,当我取消注释 Ships 结构上的注释行时,性能会下降很多。使用 MAX_ENEMY_SHIPS 的当前值,我计算机中 MoveShips 函数的持续时间从 10-11 毫秒变为 200-210 毫秒。
这里我举一个最小的、可重现的例子:
#include <stdlib.h>
#include <stdio.h>
#include <chrono>
#include <string>
class ScopeBenchmark
{
public:
ScopeBenchmark(std::string text)
: text(text)
{
begin = std::chrono::steady_clock::now();
}
~ScopeBenchmark()
{
std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();
printf("%s: %lli\n", text.data(), std::chrono::duration_cast<std::chrono::milliseconds>(end - begin).count());
}
private:
std::string text;
std::chrono::steady_clock::time_point begin;
};
constexpr int32_t Color(uint8_t r, uint8_t g, uint8_t b)
{
return (r << 16) | (g << 8) | b;
}
struct v2
{
float x;
float y;
};
inline v2 operator+(v2 a, v2 b)
{
v2 result;
result.x = a.x + b.x;
result.y = a.y + b.y;
return result;
}
inline v2 operator*(v2 a, float b)
{
v2 result;
result.x = a.x * b;
result.y = a.y * b;
return result;
}
//----------------------------------------------------------------------
struct Ship
{
v2 pos;
v2 size;
v2 dir;
float speed;
int32_t color;
v2 a;
v2 b;
v2 c;
v2 d;
};
struct ShipMovement
{
v2 dir;
float speed;
};
struct ShipDrawing
{
v2 size;
int32_t color;
};
struct ShipOtherData
{
v2 a;
v2 b;
v2 c;
v2 d;
};
constexpr int MAX_ENEMY_SHIPS = 4000000;
struct Ships
{
int32_t count;
v2 pos[MAX_ENEMY_SHIPS];
ShipMovement movements[MAX_ENEMY_SHIPS];
ShipDrawing drawings[MAX_ENEMY_SHIPS];
//ShipOtherData other[MAX_ENEMY_SHIPS];
void Add(Ship ship)
{
pos[count] = ship.pos;
movements[count] = { ship.dir, ship.speed };
drawings[count] = { ship.size, ship.color };
//other[count] = { ship.a, ship.b, ship.c, ship.d };
count++;
}
};
void MoveShips(v2* positions, ShipMovement* movements, int count)
{
ScopeBenchmark bench("Move Ships");
for(int i = 0; i < count; ++i)
{
positions[i] = positions[i] + (movements[i].dir * movements[i].speed);
}
}
struct Game
{
int32_t playerShipIndex;
Ships ships;
};
void InitGame(void* gameMemory)
{
Game* game = (Game*)gameMemory;
Ship ship;
ship.pos = { 0.0f, 0.0f };
ship.size = { 100.0f, 100.0f };
ship.speed = 1.0f;
ship.color = Color(64, 192, 32);
game->ships.Add(ship);
game->playerShipIndex = 0;
ship.speed *= 0.5f;
ship.dir.x = -1.0f;
ship.size = { 50.0f, 50.0f };
ship.color = Color(192, 64, 32);
for(int i = 0; i < MAX_ENEMY_SHIPS; i++)
{
ship.pos = { 500.0f, 350.0f };
game->ships.Add(ship);
}
}
int main()
{
Game* game = (Game*)malloc(sizeof(Game));
memset(game, 0, sizeof(Game));
InitGame(game);
while (true)
{
MoveShips(game->ships.pos, game->ships.movements, game->ships.count);
}
}
我用的是Visual Studio编译器,编译文件的命令如下:
cl.exe /O2 /GL src/Game.cpp
所以,我的问题是:为什么 MoveShips 函数的性能在添加未使用的内存时下降如此严重?
问题是您在函数调用中传递了未初始化的数据 game->ships.Add(ship)
。这导致 undefined behavior.
在第一个函数调用中,ship.dir.x
和 ship.dir.y
都未初始化。在所有进一步的函数调用中,ship.dir.y
未初始化。
如果 ship.dir.y
恰好包含代表 denormalized floating point value. See this question 以获取更多信息的垃圾数据,这会对性能产生特别负面的影响。
我能够重现您的问题,并且我的测试表明这是您性能下降的原因。通过将变量 ship.dir.y
初始化为规范化浮点值,我能够可靠地将性能提高 45(!)倍。
我认为您的问题与您将 struct
的大小增加了 un-commenting 两行代码没有任何关系。尽管在评论部分建议这可能会导致您的程序因使用 swap space 而变慢,但我的测试表明这对您的情况没有显着的性能影响。将内存分配的总大小增加到 256 MB 通常应该不是问题,除非您使用的计算机内存量非常小。因此,我相信您观察到当您 un-comment 这两行代码时性能显着下降只是巧合。
我的猜测是 address space layout randomization (ASLR) 导致您每次 运行 程序时都得到不同的垃圾值,因此它们有时表示非规范化的浮点值,有时则不是。至少那是我在测试期间所经历的:在 ASLR 处于活动状态时,我有时会得到一个非规范化的值,有时会得到一个规范化的值。但是,在禁用 ALSR 的情况下(使用 MS Visual Studio 中的 /DYNAMICBASE:NO
链接器选项),我总是得到一个非规范化的值,而不是规范化的值。
如果您确定从 un-commenting 您的代码中观察到的结果并非巧合,而是一致的,那么最可能的解释是 un-commenting 代码导致您收到不同的垃圾值,它恰好总是代表非规范化的浮点值。
因此,为了解决您的问题,您所要做的就是确保 ship.dir.x
和 ship.dir.y
在将它们传递给函数之前已正确初始化。
此外,虽然这可能不是您遇到问题的原因,但必须指出您正在向 struct Ships
中的所有 4 个数组写入越界。您恰好调用函数 game->ships.Add(ship)
次 MAX_ENEMY_SHIPS + 1
次,一次在循环外,在循环内调用 MAX_ENEMY_SHIPS
次。因此,您正好通过一个元素传递每个数组的边界。这也会导致未定义的行为。
当我偶然发现这种奇怪的性能下降时,我正在玩一个简单的“游戏”来测试面向数据设计的不同方面。
我有这个结构来存储游戏飞船的数据:
constexpr int MAX_ENEMY_SHIPS = 4000000;
struct Ships
{
int32_t count;
v2 pos[MAX_ENEMY_SHIPS];
ShipMovement movements[MAX_ENEMY_SHIPS];
ShipDrawing drawings[MAX_ENEMY_SHIPS];
//ShipOtherData other[MAX_ENEMY_SHIPS];
void Add(Ship ship)
{
pos[count] = ship.pos;
movements[count] = { ship.dir, ship.speed };
drawings[count] = { ship.size, ship.color };
//other[count] = { ship.a, ship.b, ship.c, ship.d };
count++;
}
};
然后我有更新运动数据的功能:
void MoveShips(v2* positions, ShipMovement* movements, int count)
{
ScopeBenchmark bench("Move Ships");
for(int i = 0; i < count; ++i)
{
positions[i] = positions[i] + (movements[i].dir * movements[i].speed);
}
}
我的理解是,由于 MoveShips 函数仅使用位置和移动数组,因此 Ships 结构中的额外内存不会影响其性能。但是,当我取消注释 Ships 结构上的注释行时,性能会下降很多。使用 MAX_ENEMY_SHIPS 的当前值,我计算机中 MoveShips 函数的持续时间从 10-11 毫秒变为 200-210 毫秒。
这里我举一个最小的、可重现的例子:
#include <stdlib.h>
#include <stdio.h>
#include <chrono>
#include <string>
class ScopeBenchmark
{
public:
ScopeBenchmark(std::string text)
: text(text)
{
begin = std::chrono::steady_clock::now();
}
~ScopeBenchmark()
{
std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();
printf("%s: %lli\n", text.data(), std::chrono::duration_cast<std::chrono::milliseconds>(end - begin).count());
}
private:
std::string text;
std::chrono::steady_clock::time_point begin;
};
constexpr int32_t Color(uint8_t r, uint8_t g, uint8_t b)
{
return (r << 16) | (g << 8) | b;
}
struct v2
{
float x;
float y;
};
inline v2 operator+(v2 a, v2 b)
{
v2 result;
result.x = a.x + b.x;
result.y = a.y + b.y;
return result;
}
inline v2 operator*(v2 a, float b)
{
v2 result;
result.x = a.x * b;
result.y = a.y * b;
return result;
}
//----------------------------------------------------------------------
struct Ship
{
v2 pos;
v2 size;
v2 dir;
float speed;
int32_t color;
v2 a;
v2 b;
v2 c;
v2 d;
};
struct ShipMovement
{
v2 dir;
float speed;
};
struct ShipDrawing
{
v2 size;
int32_t color;
};
struct ShipOtherData
{
v2 a;
v2 b;
v2 c;
v2 d;
};
constexpr int MAX_ENEMY_SHIPS = 4000000;
struct Ships
{
int32_t count;
v2 pos[MAX_ENEMY_SHIPS];
ShipMovement movements[MAX_ENEMY_SHIPS];
ShipDrawing drawings[MAX_ENEMY_SHIPS];
//ShipOtherData other[MAX_ENEMY_SHIPS];
void Add(Ship ship)
{
pos[count] = ship.pos;
movements[count] = { ship.dir, ship.speed };
drawings[count] = { ship.size, ship.color };
//other[count] = { ship.a, ship.b, ship.c, ship.d };
count++;
}
};
void MoveShips(v2* positions, ShipMovement* movements, int count)
{
ScopeBenchmark bench("Move Ships");
for(int i = 0; i < count; ++i)
{
positions[i] = positions[i] + (movements[i].dir * movements[i].speed);
}
}
struct Game
{
int32_t playerShipIndex;
Ships ships;
};
void InitGame(void* gameMemory)
{
Game* game = (Game*)gameMemory;
Ship ship;
ship.pos = { 0.0f, 0.0f };
ship.size = { 100.0f, 100.0f };
ship.speed = 1.0f;
ship.color = Color(64, 192, 32);
game->ships.Add(ship);
game->playerShipIndex = 0;
ship.speed *= 0.5f;
ship.dir.x = -1.0f;
ship.size = { 50.0f, 50.0f };
ship.color = Color(192, 64, 32);
for(int i = 0; i < MAX_ENEMY_SHIPS; i++)
{
ship.pos = { 500.0f, 350.0f };
game->ships.Add(ship);
}
}
int main()
{
Game* game = (Game*)malloc(sizeof(Game));
memset(game, 0, sizeof(Game));
InitGame(game);
while (true)
{
MoveShips(game->ships.pos, game->ships.movements, game->ships.count);
}
}
我用的是Visual Studio编译器,编译文件的命令如下:
cl.exe /O2 /GL src/Game.cpp
所以,我的问题是:为什么 MoveShips 函数的性能在添加未使用的内存时下降如此严重?
问题是您在函数调用中传递了未初始化的数据 game->ships.Add(ship)
。这导致 undefined behavior.
在第一个函数调用中,ship.dir.x
和 ship.dir.y
都未初始化。在所有进一步的函数调用中,ship.dir.y
未初始化。
如果 ship.dir.y
恰好包含代表 denormalized floating point value. See this question 以获取更多信息的垃圾数据,这会对性能产生特别负面的影响。
我能够重现您的问题,并且我的测试表明这是您性能下降的原因。通过将变量 ship.dir.y
初始化为规范化浮点值,我能够可靠地将性能提高 45(!)倍。
我认为您的问题与您将 struct
的大小增加了 un-commenting 两行代码没有任何关系。尽管在评论部分建议这可能会导致您的程序因使用 swap space 而变慢,但我的测试表明这对您的情况没有显着的性能影响。将内存分配的总大小增加到 256 MB 通常应该不是问题,除非您使用的计算机内存量非常小。因此,我相信您观察到当您 un-comment 这两行代码时性能显着下降只是巧合。
我的猜测是 address space layout randomization (ASLR) 导致您每次 运行 程序时都得到不同的垃圾值,因此它们有时表示非规范化的浮点值,有时则不是。至少那是我在测试期间所经历的:在 ASLR 处于活动状态时,我有时会得到一个非规范化的值,有时会得到一个规范化的值。但是,在禁用 ALSR 的情况下(使用 MS Visual Studio 中的 /DYNAMICBASE:NO
链接器选项),我总是得到一个非规范化的值,而不是规范化的值。
如果您确定从 un-commenting 您的代码中观察到的结果并非巧合,而是一致的,那么最可能的解释是 un-commenting 代码导致您收到不同的垃圾值,它恰好总是代表非规范化的浮点值。
因此,为了解决您的问题,您所要做的就是确保 ship.dir.x
和 ship.dir.y
在将它们传递给函数之前已正确初始化。
此外,虽然这可能不是您遇到问题的原因,但必须指出您正在向 struct Ships
中的所有 4 个数组写入越界。您恰好调用函数 game->ships.Add(ship)
次 MAX_ENEMY_SHIPS + 1
次,一次在循环外,在循环内调用 MAX_ENEMY_SHIPS
次。因此,您正好通过一个元素传递每个数组的边界。这也会导致未定义的行为。