Я недавно обнаружил, что некоторые люди, которым мы доверяем производительность операций с GPU на стороне процессора,.. не умеют или не всегда хотят верно бенчмаркить.Вот их бенчмарк: https://github.com/vkmark/vkmark (анализ для коммита 2c5f020005a597fb4b7d9ed1eaa1bf40e466bf01, все лицензии на код находятся в соответствующих репозиториях)
>vkmark offers a suite of scenes that can be used to measure various aspects of Vulkan performance.
https://www.collabora.com/news-and-blog/blog/2017/07/18/vkma.../
>A few years ago we were pleasantly surprised to learn that developers were using glmark2 as a testing tool for driver development, especially in free (as in freedom) software projects. This is a goal that we want to actively pursue for vkmark, too.
Давайте посмотрим, как эти понтовые заявления соотносятся с реальностью.
```c++
// With a lot of contractions
class Scene
{
public:
...
virtual bool is_valid() const;
...
virtual VulkanImage draw(VulkanImage const&);
...
virtual void update();
...
unsigned int average_fps() const;
...
};
```
1. Виртуальные функции, Карл!
2. `unsigned int average_fps() const;` - СРЕДНЕЕ. FPS.
3. Функции не являются inline. И не force-inlined.
```c++
scene.start();
while (scene.is_running() &&
!(should_quit = ws.should_quit()) &&
!should_stop)
{
ws.present_vulkan_image(
scene.draw(ws.next_vulkan_image()));
scene.update();
}
auto const scene_fps = scene.average_fps();
log_scene_fps(scene_fps);
```
Где находятся функции (в отдельном translation unit):
```c++
void Scene::start()
{
current_frame = 0;
running = true;
start_time = Util::get_timestamp_us();
last_update_time = start_time;
}
unsigned int Scene::average_fps() const
{
return last_update_time > start_time ?
current_frame * 1000000 / (last_update_time - start_time) : 0;
}
bool Scene::is_running() const
{
return running;
}
VulkanImage Scene::draw(VulkanImage const& image)
{
return image.copy_with_semaphore({});
}
void Scene::update()
{
auto const current_time = Util::get_timestamp_us();
auto const elapsed_time = current_time - start_time;
++current_frame;
last_update_time = current_time;
if (elapsed_time >= duration)
running = false;
}
void log_scene_fps(unsigned int fps)
{
auto const fmt = Log::continuation_prefix + " FPS: %u FrameTime: %.3f ms\n";
Log::info(fmt.c_str(), fps, 1000.0 / fps);
Log::flush();
}
uint64_t Util::get_timestamp_us()
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
uint64_t const now = static_cast<uint64_t>(ts.tv_sec) * 1000000 +
static_cast<uint64_t>(ts.tv_nsec) / 1000;
return now;
}
```
ОК, предположим, что компилятор и линковщик достаточно умны, чтобы их заинлайнить, поэтому давайте заинлайним их вручную, и глянем, что получается...
```c++
current_frame = 0;
running = true;
start_time = Util::get_timestamp_us();
last_update_time = start_time;
while (running &&
!(should_quit = ws.should_quit()) &&
!should_stop)
{
auto image = ws.next_vulkan_image();
VulkanImage draw_result = image.copy_with_semaphore({});
ws.present_vulkan_image(draw_result);
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
uint64_t const current_time = static_cast<uint64_t>(ts.tv_sec) * 1000000 + static_cast<uint64_t>(ts.tv_nsec) / 1000;
uint64_t const elapsed_time = current_time - start_time;
++current_frame;
last_update_time = current_time;
if (elapsed_time >= duration){
running = false;
}
}
unsigned int const scene_fps = last_update_time > start_time ? current_frame * 1000000 / (last_update_time - start_time) : 0;
auto const fmt = Log::continuation_prefix + " FPS: %u FrameTime: %.3f ms\n";
Log::info(fmt.c_str(), scene_fps, 1000.0 / scene_fps);
Log::flush();
```
1. Мы измеряем не FPS, а время. Но... они сначала вычисляют FPS из времени, усекают его до целого числа, затем время из FPS, и также усекают его. Это была проблема, которую я заметил первой, что заставило меня покопаться в этом.
2. Они используют `clock_gettime` вместо `__rdtsc` + `_mm_lfence` для получения точных тиков ЦП. Ни один разумный микро-бенчмарк не полагается на время, предоставляемое через системные вызовы.
3. В коде измерения времени - ненужные операции.
4. Мы видим ненужные операции между точками, где мы получаем время. В идеале `ws.present_vulkan_image(scene.draw(ws.next_vulkan_image()));` должно быть сендвичем между двумя гаджетами
```c++
_mm_lfence();
const uint64_t current_time = __rdtsc();
_mm_lfence();
```
.
6. Вместо измерения времени отдельных операций, они проводят бенчмаркинг в течение заранее заданного периода времени. Это противоположно тому, как обычно делают бенчмаркинг.
7. Они вычисляют средний FPS, используя `last_update_time - start_time` в качестве времени, а не сумму точных интервалов.
8. Никакой коррекции систематической погрешности, вносимой самими инструкциями измерения времени.
Мы просто предположили инлайнинг, но разумеется, компиляторы не настолько умны для этого. Для правильного бенчмаркинга им понадобится `MainLoop` в качестве шаблона и предоставление реализаций сцен в качестве аргументов шаблона, тогда компилятор заинлайнит реализацию прямо в горячий цикл.
Так что утверждение о том, что этот бенчмарк можно использовать каким-либо образом для чего-либо полезного, не выдерживает никакой критики.
Вот небольшой патч https://0x0.st/8kvd.patch , просто запихивающий `__rdtsc` туда, без каких-либо других необходимых изменений этого. И график гистограммы (нормализованной до плотности вероятности) времен (оставлен только первый пик, и на самом деле измерения расположены с 8 тиками, так что 8 тиков с количеством 0, затем тик с количеством, а затем снова 8 тиков с количеством 0 и тик с количеством) и соответствующая kernel density estimation. 2 изображения, одно в крупном масштабе, другое увеличено возле моды распределения, обратите внимание, что счетчики гистограммы очень шумные, более чем в два раза больше моды на ее пике. Команда была `./src/vkmark -D <device uuid> -p immediate -b effect2d:kernel=none:duration=4200000 --run-forever > results.txt`, после чего парсинг скриптом.
Вот что вам нужно знать об инженере из Collabora, который это разработал.
О, может быть, другие бенчмарки дадут лучшие результаты? Я нашел `https://github.com/RippeR37/GL_vs_VK` (коммит 735d153f7d85b7ce8d7097e89e877304ee23180b) , понтовую часть Phoronics Test Suite.
>This project is part of my master-thesis and aims to compare OpenGL and Vulkan API in terms of API-related overhead.
Если вы думаете, что этот бенчмарк отличается... нет, он в основном повторяет те же ошибки (за исключением виртуальных функций): неиспользование `rdtsc` напрямую (вместо этого он использует `glfwGetTime` из динамической библиотеки, определенной как
```c++
GLFWAPI double glfwGetTime(void)
{
_GLFW_REQUIRE_INIT_OR_RETURN(0.0);
return (double) (_glfwPlatformGetTimerValue() - _glfw.timer.offset) / _glfwPlatformGetTimerFrequency();
}
```
где
```c++
uint64_t _glfwPlatformGetTimerValue(void)
{
struct timespec ts;
clock_gettime(_glfw.timer.posix.clock, &ts);
return (uint64_t) ts.tv_sec * _glfw.timer.posix.frequency + (uint64_t) ts.tv_nsec;
}
```
- да, те же яйца, только в профиль), фактический вызов получения времени находится в отдельной функции в отдельном translation unit, а не заинлайнен в нужное место.