Simplify container iteration using lambdas

KaiserJohaan

I have two functions, functionA and functionB, that both iterate over a container (std::vector) and perform some work:

void functionA() {
  // ...........

  auto meshIterator = mMeshes.begin();
  for (const Renderable &renderable : renderQueue) {
    if (renderable.mMesh == INVALID_MESH_ID) {
      JONS_LOG_ERROR(mLogger, "Renderable MeshID is invalid");
      throw std::runtime_error("Renderable MeshID is invalid");
    }

    if (renderable.mMesh < meshIterator->first->mMeshID)
      continue;

    while (renderable.mMesh > meshIterator->first->mMeshID) {
      meshIterator++;
      if (meshIterator == mMeshes.end()) {
        JONS_LOG_ERROR(mLogger, "Renderable MeshID out of range");
        throw std::runtime_error("Renderable MeshID out of range");
      }
    }

    const bool hasDiffuseTexture =
        renderable.mDiffuseTexture != INVALID_TEXTURE_ID;
    const bool hasNormalTexture =
        renderable.mNormalTexture != INVALID_TEXTURE_ID;

    mGeometryProgram.SetUniformData(
        UnifGeometry(renderable.mWVPMatrix, renderable.mWorldMatrix,
                     hasDiffuseTexture, hasNormalTexture,
                     renderable.mTextureTilingFactor));

    if (hasDiffuseTexture)
      BindTexture2D(OpenGLTexture::TEXTURE_UNIT_GEOMETRY_DIFFUSE,
                    renderable.mDiffuseTexture, mTextures, mLogger);

    if (hasNormalTexture)
      BindTexture2D(OpenGLTexture::TEXTURE_UNIT_GEOMETRY_NORMAL,
                    renderable.mNormalTexture, mTextures, mLogger);

    GLCALL(glBindVertexArray(meshIterator->second));
    GLCALL(glDrawElements(GL_TRIANGLES, meshIterator->first->mIndices,
                          GL_UNSIGNED_INT, 0));
    GLCALL(glBindVertexArray(0));
  }

  // ...........
}

void functionB() {
  //....................

  // both containers are assumed to be sorted by MeshID ascending
  auto meshIterator = mMeshes.begin();
  for (const Renderable &renderable : renderQueue) {
    if (renderable.mMesh == INVALID_MESH_ID) {
      JONS_LOG_ERROR(mLogger, "Renderable MeshID is invalid");
      throw std::runtime_error("Renderable MeshID is invalid");
    }

    if (renderable.mMesh < meshIterator->first->mMeshID)
      continue;

    while (renderable.mMesh > meshIterator->first->mMeshID) {
      meshIterator++;
      if (meshIterator == mMeshes.end()) {
        JONS_LOG_ERROR(mLogger, "Renderable MeshID out of range");
        throw std::runtime_error("Renderable MeshID out of range");
      }
    }

    const Mat4 wvp = lightVP * renderable.mWorldMatrix;
    mNullProgram.SetUniformData(UnifNull(wvp));

    GLCALL(glBindVertexArray(meshIterator->second));
    GLCALL(glDrawElements(GL_TRIANGLES, meshIterator->first->mIndices,
                          GL_UNSIGNED_INT, 0));
    GLCALL(glBindVertexArray(0));
  }

  // ...............
}

They way they iterate over the container is very similiar, but the work they do in the body is very different. I would like to join these two (possibly more in the future) in a single function instead, like this:

void DrawModels(const std::function<
    void(const Renderable &renderable)> &preDrawFunc) {
  // both containers are assumed to be sorted by MeshID ascending
  auto meshIterator = mMeshes.begin();
  for (const Renderable &renderable : renderQueue) {
    if (renderable.mMesh == INVALID_MESH_ID) {
      JONS_LOG_ERROR(mLogger, "Renderable MeshID is invalid");
      throw std::runtime_error("Renderable MeshID is invalid");
    }

    if (renderable.mMesh < meshIterator->first->mMeshID)
      continue;

    while (renderable.mMesh > meshIterator->first->mMeshID) {
      meshIterator++;
      if (meshIterator == mMeshes.end()) {
        JONS_LOG_ERROR(mLogger, "Renderable MeshID out of range");
        throw std::runtime_error("Renderable MeshID out of range");
      }
    }

    preDrawFunc(renderable);

    GLCALL(glBindVertexArray(meshIterator->second));
    GLCALL(glDrawElements(GL_TRIANGLES, meshIterator->first->mIndices,
                          GL_UNSIGNED_INT, 0));
    GLCALL(glBindVertexArray(0));
  }
}

I reason the std::function provided can be used to perform some arbitrary work depending on the caller, like this:

void functionD() {
  auto preDrawRenderable = [&](const Renderable &renderable) {
    const bool hasDiffuseTexture =
        renderable.mDiffuseTexture != INVALID_TEXTURE_ID;
    const bool hasNormalTexture =
        renderable.mNormalTexture != INVALID_TEXTURE_ID;

    mGeometryProgram.SetUniformData(
        UnifGeometry(renderable.mWVPMatrix, renderable.mWorldMatrix,
                     hasDiffuseTexture, hasNormalTexture,
                     renderable.mTextureTilingFactor));

    if (hasDiffuseTexture)
      BindTexture2D(OpenGLTexture::TEXTURE_UNIT_GEOMETRY_DIFFUSE,
                    renderable.mDiffuseTexture, mTextures, mLogger);

    if (hasNormalTexture)
      BindTexture2D(OpenGLTexture::TEXTURE_UNIT_GEOMETRY_NORMAL,
                    renderable.mNormalTexture, mTextures, mLogger);
  };

  DrawModels(preDrawRenderable);
}

void functionE() {
  auto preDrawRenderable = [&](const Renderable &renderable) {
    const Mat4 wvp = lightVP * renderable.mWorldMatrix;
    mNullProgram.SetUniformData(UnifNull(wvp));
  };

  DrawModels(preDrawRenderable);
}

My questions:

1) functionD and functionE will both need to run about 60-100 times per seconds. Does using lambda and std::function incur any significant performance penalties? For example, are there any hidden dynamic memory allocation calls or virtual lookups or whatnot that could trash performance? I don't know the overhead of using lambdas and std::function.

2) Is there a better/faster/cleaner alternative than my naive solution?

Yakk - Adam Nevraumont
void DrawModels(const std::function< void(const Renderable &renderable)> &preDrawFunc)

instead of that, do this:

template<class RenderableFunc>
void DrawModels(RenderableFunc&& preDrawFunc)

and leave the body unchanged. Place it where both of the functions you are replacing can see it.

Now the compiler has an easy optimization problem to inline your lambda.

std::function is a type erasure object that does magic that will confuse current generation optimizers. It is not the type of a lambda, it is a type that can convert any lambda or function pointer or invokable object into an internal object and store it for later execution.

A raw lambda is a compiler generated function object with captured variables and a non-virtual operaror(). It is much lighter weight.

Collected from the Internet

Please contact [email protected] to delete if infringement.

edited at
0

Comments

0 comments
Login to comment

Related