Упрощение итерации контейнера с использованием лямбд

#c #c 11 #lambda #iteration

#c #c 11 #лямбда #итерация

Вопрос:

У меня есть две функции, functionA и functionB, которые выполняют итерацию по контейнеру (std::vector) и выполняют некоторую работу:

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

  auto meshIterator = mMeshes.begin();
  for (const Renderable amp;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 amp;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));
  }

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

То, как они перебирают контейнер, очень похоже, но работа, которую они выполняют в теле, сильно отличается. Вместо этого я хотел бы объединить эти два (возможно, больше в будущем) в одну функцию, например:

 void DrawModels(const std::function<
    void(const Renderable amp;renderable)> amp;preDrawFunc) {
  // both containers are assumed to be sorted by MeshID ascending
  auto meshIterator = mMeshes.begin();
  for (const Renderable amp;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));
  }
}
  

Я считаю, что предоставленная std::function может использоваться для выполнения некоторой произвольной работы в зависимости от вызывающего, например:

 void functionD() {
  auto preDrawRenderable = [amp;](const Renderable amp;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 = [amp;](const Renderable amp;renderable) {
    const Mat4 wvp = lightVP * renderable.mWorldMatrix;
    mNullProgram.SetUniformData(UnifNull(wvp));
  };

  DrawModels(preDrawRenderable);
}
  

Мои вопросы:

1) functionD и functionE должны выполняться примерно 60-100 раз в секунду. Влечет ли использование lambda и std::function какие-либо значительные потери производительности? Например, существуют ли какие-либо скрытые вызовы динамического выделения памяти или виртуальные запросы или что-то еще, что может снизить производительность? Я не знаю накладных расходов на использование лямбд и std::function.

2) Есть ли лучшая / более быстрая / более чистая альтернатива, чем мое наивное решение?

Комментарии:

1. Единственный разумный способ реализации std::function — это динамическое выделение памяти и вызов виртуальной функции. У лямбд нет этой проблемы.

2. @nwp Эти два лямбда на самом деле ничего не фиксируют (несмотря на то, что им нечего фиксировать [amp;] ), поэтому реализация std::function может избежать выделения памяти и просто сохранить указатель на функцию.

3. разве нет? Что такое переменные-члены «mGeometryProgram», «mTextures», «mLogger»?

Ответ №1:

 void DrawModels(const std::function< void(const Renderable amp;renderable)> amp;preDrawFunc)
  

вместо этого сделайте это:

 template<class RenderableFunc>
void DrawModels(RenderableFuncamp;amp; preDrawFunc)
  

и оставьте тело без изменений. Поместите его так, чтобы его могли видеть обе функции, которые вы заменяете.

Теперь у компилятора есть простая задача оптимизации для встраивания вашего лямбда.

std::function это объект стирания типов, который творит волшебство, которое сбивает с толку оптимизаторы текущего поколения. Это не тип лямбда-выражения, это тип, который может преобразовать любой лямбда-выражение или указатель на функцию или вызываемый объект во внутренний объект и сохранить его для последующего выполнения.

Необработанный лямбда- это функциональный объект, сгенерированный компилятором с захваченными переменными и неоперационным virtual () . Это намного легче.

Комментарии:

1. Вызов функции, подобной этой «DrawModels<decltype(preDrawRenderable)> (preDrawRenderable);» дает «Вы не можете привязать значение lvalue к ссылке rvalue» с помощью VS2013 — почему это?

2. @kaiserjohaan потому что я использовал универсальную ссылку, предназначенную для вывода с помощью аргумента. Если вам необходимо передать тип, передавайте <decltype(preDrawRenderable) constamp;> , если аргумент не является move общим. Но на самом деле просто дайте ему быть выведенным.

Ответ №2:

Влечет ли использование lambda и std::function какие-либо значительные потери производительности?

Лямбды — это только анонимные функторы, созданные на месте. Поэтому не должно быть заметного снижения производительности по сравнению с вашими собственными функциями / функторами.

С другой стороны std::function , применяется стирание типов, но обычно (это определяется реализацией) вместо полиморфизма используется приведение и отправка тегов. Поэтому могут быть некоторые потери производительности по сравнению с обычной функцией, но они могут быть незначительными. РЕДАКТИРОВАТЬ: как указал Якк в своем ответе, удаление типа может нарушить способность компилятора встроить функцию.

Есть ли лучшая / более быстрая / более чистая альтернатива, чем мое наивное решение?

Может быть более элементарным полагаться на стандартные алгоритмы ( <algorithm> заголовок) вместо необработанных циклов. Это делает код более читаемым, но прямого улучшения производительности нет.

Обратите внимание, что некоторые советы, которые я написал здесь, могут рассматриваться как немного субъективные. Как и в любом вопросе, связанном с производительностью, не полагайтесь на «здравый смысл», измеряйте и выполняйте профилирование производительности. Например, как я уже сказал, написание кода на основе алгоритмов очищает код, но не может привести к прямому повышению производительности. Это сильно зависит от контекста, поэтому просто profile .