Simplemente no lo copie / Sudo Null IT News

De lo que voy a hablar en este artículo es tan trivial que cualquier desarrollador, incluso un principiante, ya lo sabe; realmente eso espero. Sin embargo, el código recibido para revisión muestra que la gente ha hecho y continúa haciendo algo similar:

bool LoadAnimation(str::string filename);
void DrawLines(std::vector<Points> path);
Matrix RotateObject(Matrix m, Angle angle);
int DrawSprite(Sprite sprite);

¿Qué tienen estas funciones en común? Argumento por valor. Y cada vez que se llama a dicha funcionalidad en el código, se crea una copia de los datos de entrada en su contexto temporal y se pasa dentro de la función. Y también puede perdonar el código que rara vez se llama, como cargar animaciones o confiar en el compilador para que las optimizaciones funcionen y destruya la copia de datos, pero por suerte, la mayoría de las veces este enfoque de desarrollo solo destruye el rendimiento y los fps.


Cualquier optimización debe abordarse. ¡solo! Después de analizarlas en el generador de perfiles, resultó que las copias pueden no ser una operación costosa. Por ejemplo, esto depende del tamaño del objeto, por lo que el compilador hace un excelente trabajo al pasar objetos de hasta 32 bytes por valor; por supuesto, hay costos, pero son muy insignificantes y no quedan atrapados en los puntos de referencia. El proveedor puede estropear “algo en la plataforma y el compilador”: copiar 32 kb de áreas de memoria especiales será más rápido que sumar un par de números. Y en el juego en sí, la optimización del código activo, seamos honestos, a menudo no es el mayor problema en el rendimiento general. Pero la asignación de memoria dinámica puede presentar muchas sorpresas, especialmente cuando se usa sin pensar.

Pero incluso si la sobrecarga es pequeña, ¿tiene algún sentido desperdiciar ciclos de CPU cuando se puede evitar? Estos “2-3% perdidos” de perforación manchada, que no brillan ni siquiera en el perfilador, son muy difíciles de detectar más adelante y aún más difíciles de reparar.

Ubicación oculta en líneas
#include <string>
#include <numeric>

size_t PassStringByValueImpl(std::string str) {
    return std::accumulate(str.begin(), str.end(), 0, () (size_t v, char a) { 
        return (v += (a == ' ') ? 1 : 0);
    });
}

size_t PassStringByRefImpl(const std::string& str) {
    return std::accumulate(str.begin(), str.end(), 0, () (size_t v, char a) { 
        return (v += (a == ' ') ? 1 : 0);
    });
}

const std::string LONG_STR("a long string that can't use Small String Optimization");

void PassStringByValue(benchmark::State& state) {
    for (auto _ : state) {
        size_t n = PassStringByValueImpl(LONG_STR);
        benchmark::DoNotOptimize(n);
    }
}
BENCHMARK(PassStringByValue);

void PassStringByRef(benchmark::State& state) {
    for (auto _ : state) {
        size_t n = PassStringByRefImpl(LONG_STR);
        benchmark::DoNotOptimize(n);
    }
}
BENCHMARK(PassStringByRef);

void PassStringByNone(benchmark::State& state) {
    for (auto _ : state) {
        size_t n = 0;
        benchmark::DoNotOptimize(n);
    }
}
BENCHMARK(PassStringByNone);

Banco rápido: https://quick-bench.com/q/f6sBUE7FdwLdsU47G26yPOnnViY

Ubicación oculta en matrices
size_t SumValueImpl(std::vector<unsigned> vect)
{
    size_t sum = 0;
    for(unsigned val: vect) { sum += val; }
    return sum;
}

size_t SumRefImpl(const std::vector<unsigned>& vect)
{
    size_t sum = 0;
    for(unsigned val: vect) { sum += val; }
    return sum;
}

const std::vector<unsigned> vect_in = { 1, 2, 3, 4, 5 };

void PassVectorByValue(benchmark::State& state) {
    for (auto _ : state) {
        size_t n = SumValueImpl(vect_in);
        benchmark::DoNotOptimize(n);
    }
}
BENCHMARK(PassVectorByValue);

void PassVectorByRef(benchmark::State& state) {
    for (auto _ : state) {
        size_t n = SumRefImpl(vect_in);
        benchmark::DoNotOptimize(n);
    }
}
BENCHMARK(PassVectorByRef);

void PassVectorByNone(benchmark::State& state) {
    for (auto _ : state) {
        size_t n = 0;
        benchmark::DoNotOptimize(n);
    }
}
BENCHMARK(PassVectorByNone);

Banco rápido: https://quick-bench.com/q/GU68xgT0r97eYaCKxMzm9bXJei4

El compilador es aún más inteligente.

En los casos en que el objeto copiado tenga un tamaño inferior a un par de decenas de bytes, no notaremos ninguna diferencia al pasarlo por referencia, hasta el punto de que el código ensamblador generado será “casi el mismo”.

Hay copia, pero no afecta.
struct Vector{
    double x;
    double y;
    double z;
};

double DotProductObjectImpl(Vector a, Vector p){
    return (a.x*p.x + a.y*p.y + a.z*p.z);
}

double DotProductRefImpl(const Vector& a, const Vector& p){
    return (a.x*p.x + a.y*p.y + a.z*p.z);
}

void DotProductObject(benchmark::State& state) {
    Vector in = {1,2,3};
    for (auto _ : state) {
        double dp = DotProductObjectImpl(in, in);
        benchmark::DoNotOptimize(dp);
    }
}
BENCHMARK(DotProductObject);

void DotProductRef(benchmark::State& state) {
    Vector in = {1,2,3};
    for (auto _ : state) {
        double dp = DotProductObjectImpl(in, in);
        benchmark::DoNotOptimize(dp);
    }
}
BENCHMARK(DotProductRef);

void DotProductNone(benchmark::State& state) {
    for (auto _ : state) {
        size_t n = 0;
        benchmark::DoNotOptimize(n);
    }
}
BENCHMARK(DotProductNone);

Banco rápido: https://quick-bench.com/q/drlH-a9o4ejvWP87neq7KAyyA8o

En este ejemplo, por supuesto, conocemos el tamaño de la estructura y el ejemplo es muy simple. Por otro lado, si pasar por referencia claramente no es más lento que pasar por valor, usar const& será “lo mejor que podamos”. Y la transferencia de tipos primitivos por const& y no afecta nada en absoluto compilacion con bandera /Ox

Y como no hay ventajas, escribe algo como esto. const int &i No tiene sentido, pero algunos todavía escriben.

Reserva mi vector

Las matrices tienen una gran ventaja sobre otras estructuras de datos que a menudo anulan las comodidades de otros contenedores: sus elementos se suceden unos a otros en la memoria. Podemos discutir durante mucho tiempo el impacto del algoritmo utilizado en el tiempo de funcionamiento y cómo esto puede afectar el rendimiento, pero no tenemos nada más rápido que los cachés del procesador, y cuantos más elementos haya en el caché, más rápido será lo más banal. la búsqueda funcionará. Cualquier acceso fuera de la caché L1 aumenta inmediatamente el tiempo de funcionamiento.

Pero cuando trabajan con vectores (matrices dinámicas), muchas personas olvidan o no recuerdan lo que hay debajo del capó. Y allí, si el espacio asignado se acaba y, por ejemplo, se asignó para 1 (un) elemento, entonces:

  1. Se asigna un nuevo bloque de memoria que es más grande.

  2. Se copian todos los elementos que se guardaron en el nuevo bloque.

  3. El antiguo bloque de memoria se elimina.

Todas estas operaciones son costosas, muy rápidas, pero siguen siendo costosas. Y ocurren debajo del capó y no son visibles:
– si no pierdes el tiempo, mira el código de la biblioteca estándar
– no mires el perfilador de ubicación
– confiar en el código escrito por el proveedor (aunque aquí hay que aceptar las cosas como son)

Usa reserva, Luke
static void NoReserve(benchmark::State& state) 
{
  for (auto _ : state) {
    // create a vector and add 100 elements
    std::vector<size_t> v;
    for(size_t i=0; i<100; i++){  v.push_back(i);  }
    benchmark::DoNotOptimize(v);
  }
}
BENCHMARK(NoReserve);

static void WithReserve(benchmark::State& state) 
{
  for (auto _ : state) {
    // create a vector and add 100 elements, but reserve first
    std::vector<size_t> v;
    v.reserve(100);
    for(size_t i=0; i<100; i++){  v.push_back(i);  }
    benchmark::DoNotOptimize(v);
  }
}
BENCHMARK(WithReserve);

static void CycleNone(benchmark::State& state) {
  // create the vector only once
  std::vector<size_t> v;
  for (auto _ : state) {
    benchmark::DoNotOptimize(v);
  }
}
BENCHMARK(CycleNone);

Banco rápido: https://quick-bench.com/q/OuiFp3VOZKNKaAZgM_0DkJxRock

Y finalmente, un ejemplo de cómo puedes matar a un perf y obtener una caída de hasta 10 FPS de la nada, simplemente moviendo el mouse por el campo de juego. No nombraré el motor, el error ya se ha solucionado. Si encuentras algún error escribe en los comentarios 🙂

bool findPath(Vector2 start, Vector2 finish) {
   ...

   while (toVisit.empty() == false) 
   {
      ...

      if (result == OBSTACLE_OBJECT_IN_WAY)
      {
         ...

         const std::vector<Vector2> directions{ {1.f, 0.f}, {-1.f, 0.f}, {0.f, 1.f}, {0.f, -1.f} };
         for (const auto& dir : directions)
         {
            auto nextPos = currentPosition + dir;
            if (visited.find(nextPos) == visited.end())
            {
               toVisit.push({ nextPos, Vector2::DistanceSq(center, nextPos) });
            }
         }
      }
   }
}

Publicaciones Similares

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *