¿Qué hacen los compiladores con la ramificación en tiempo de compilación?


EDIT: Tomé el caso "if/else" como un ejemplo que a veces se puede resolver en tiempo de compilación (por ejemplo, cuando los valores estáticos están involucrados, cf <type_traits>). Adaptar las respuestas a continuación a otros tipos de ramificaciones estáticas (por ejemplo, ramas múltiples o ramas con múltiples criterios) debe ser sencillo. Tenga en cuenta que la bifurcación en tiempo de compilación usando programación template-meta no es el tema aquí.


En un código típico como este

#include <type_traits>

template <class T>
T numeric_procedure( const T& x )
{
    if ( std::is_integral<T>::value )
    {
        // Integral types
    }
    else
    {
        // Floating point numeric types
    }
}

Optimizará el compilador el if / else ¿cuándo defino tipos de plantilla específicos más adelante en mi código?

Una alternativa simple sería escribir algo como esto:

#include <type_traits>

template <class T>
inline T numeric_procedure( const T& x )
{
    return numeric_procedure_impl( x, std::is_integral<T>() );
}

// ------------------------------------------------------------------------

template <class T>
T numeric_procedure_impl( const T& x, std::true_type const )
{
    // Integral types
}

template <class T>
T numeric_procedure_impl( const T& x, std::false_type const )
{
    // Floating point numeric types
}

Hay una diferencia en términos de rendimiento entre estas soluciones? ¿Hay algún motivo no subjetivo para decir que uno es mejor que el otro? ¿Hay otras (posiblemente mejores) soluciones para lidiar con la ramificación en tiempo de compilación?

Author: Sheljohn, 2014-05-31

4 answers

TL;DR{[19]]}

Hay varias maneras de obtener diferentes comportamientos en tiempo de ejecución que dependen de un parámetro de plantilla. El rendimiento no debe ser su principal preocupación aquí, pero la flexibilidad y la capacidad de mantenimiento deben. En todos los casos, los diversos envoltorios finos y las expresiones condicionales constantes se optimizarán en cualquier compilador decente para compilaciones de versiones. A continuación un pequeño resumen con las diversas compensaciones (inspirado en esta respuesta por @AndyProwl).

Tiempo de ejecución si

Su primera solución es el simple tiempo de ejecución if:

template<class T>
T numeric_procedure(const T& x)
{
    if (std::is_integral<T>::value) {
        // valid code for integral types
    } else {
        // valid code for non-integral types,
        // must ALSO compile for integral types
    }
}

Es simple y efectivo: cualquier compilador decente optimizará la rama muerta.

Hay varias desventajas:

  • en algunas plataformas (MSVC), una expresión condicional constante produce una advertencia falsa del compilador que luego debe ignorar o silenciar.
  • Pero peor aún, en todas las plataformas conformes, ambas ramas de la declaración if/else necesitan para compilar realmente para todos los tipos T, incluso si se sabe que una de las ramas no se toma. Si T contiene diferentes tipos de miembros dependiendo de su naturaleza, entonces obtendrá un error del compilador tan pronto como intente acceder a ellos.

Envío de etiquetas

Su segundo acercamiento se conoce como tag-dispatching:

template<class T>
T numeric_procedure_impl(const T& x, std::false_type)
{
    // valid code for non-integral types,
    // CAN contain code that is invalid for integral types
}    

template<class T>
T numeric_procedure_impl(const T& x, std::true_type)
{
    // valid code for integral types
}

template<class T>
T numeric_procedure(const T& x)
{
    return numeric_procedure_impl(x, std::is_integral<T>());
}

Funciona bien, sin sobrecarga de tiempo de ejecución: la función temporal std::is_integral<T>() y la llamada a la función auxiliar de una línea se optimizarán de manera plataforma decente.

La principal (menor IMO) desventaja es que tiene un poco de repetición con 3 en lugar de 1 función.

SFINAE

Estrechamente relacionado con el envío de etiquetas es SFINAE (Fallo de sustitución no es un error)

template<class T, class = typename std::enable_if<!std::is_integral<T>::value>::type>
T numeric_procedure(const T& x)
{
    // valid code for non-integral types,
    // CAN contain code that is invalid for integral types
}    

template<class T, class = typename std::enable_if<std::is_integral<T>::value>::type>
T numeric_procedure(const T& x)
{
    // valid code for integral types
}

Esto tiene el mismo efecto que el envío de etiquetas, pero funciona ligeramente diferente. En lugar de usar argumento-deducción para seleccionar la sobrecarga de ayudante adecuada, manipula directamente el conjunto de sobrecarga para su función principal.

El la desventaja es que puede ser una forma frágil y complicada si no sabes exactamente cuál es todo el conjunto de sobrecarga (por ejemplo, con código pesado de plantilla, ADL podría generar más sobrecargas de espacios de nombres asociados en los que no pensaste). Y en comparación con el envío de etiquetas, la selección basada en cualquier cosa que no sea una decisión binaria es mucho más involucrada.

Especialización parcial

Otro enfoque es usar un ayudante de plantilla de clase con un operador de aplicación de función y parcialmente especializarlo

template<class T, bool> 
struct numeric_functor;

template<class T>
struct numeric_functor<T, false>
{
    T operator()(T const& x) const
    {
        // valid code for non-integral types,
        // CAN contain code that is invalid for integral types
    }
};

template<class T>
struct numeric_functor<T, true>
{
    T operator()(T const& x) const
    {
        // valid code for integral types
    }
};

template<class T>
T numeric_procedure(T const& x)
{
    return numeric_functor<T, std::is_integral<T>::value>()(x);
}

Este es probablemente el enfoque más flexible si desea tener un control detallado y una duplicación de código mínima (por ejemplo, si también desea especializarse en tamaño y/o alineación, pero digamos solo para tipos de coma flotante). La coincidencia de patrones dada por la especialización parcial de plantillas es ideal para estos problemas avanzados. Al igual que con el envío de etiquetas, los funtores auxiliares son optimizados por cualquier compilador decente.

La principal desventaja es La un poco más grande caldera-placa si solo desea especializarse en una sola condición binaria.

If constexpr (propuesta de C++1z)

Esto es un reiniciar de propuestas anteriores fallidas para static if (que se utiliza en el lenguaje de programación D)

template<class T>
T numeric_procedure(const T& x)
{
    if constexpr (std::is_integral<T>::value) {
        // valid code for integral types
    } else {
        // valid code for non-integral types,
        // CAN contain code that is invalid for integral types
    }
}

Como con su tiempo de ejecución if, todo está en un solo lugar, pero la principal ventaja es que el else rama será abandonado totalmente por el compilador cuando se sabe que no se debe tomar. Una gran ventaja es que mantenga todo el código local, y no tenga que usar pequeñas funciones auxiliares como en el envío de etiquetas o la especialización parcial de plantillas.

Concepts-Lite (propuesta de C++1z)

Concepts-Lite es un próxima Especificación Técnica eso está programado para ser parte de la próxima versión principal de C++ (C++1z, con z==7 como la mejor suposición).

template<Non_integral T>
T numeric_procedure(const T& x)
{
    // valid code for non-integral types,
    // CAN contain code that is invalid for integral types
}    

template<Integral T>
T numeric_procedure(const T& x)
{
    // valid code for integral types
}

Este enfoque reemplaza la palabra clave class o typename dentro de los corchetes template< > con un nombre conceptual que describe la familia de tipos para los que se supone que funciona el código. Puede ser visto como una generalización de las técnicas de tag-dispatching y SFINAE. Algunos compiladores (gcc, Clang) tienen soporte experimental para esta característica. El adjetivo Lite se refiere a la propuesta fallida de Concepts C++11.

 45
Author: TemplateRex,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:09:56

Tenga en cuenta que aunque el optimizador bien puede ser capaz de podar pruebas estáticamente conocidas y ramas inalcanzables del código generado, el compilador todavía necesita ser capaz de compilar cada rama.

Esto es:

int foo() {
  #if 0
    return std::cout << "this isn't going to work\n";
  #else
    return 1;
  #endif
}

Funcionará bien, porque el preprocesador elimina la rama muerta antes de que el compilador la vea, pero:

int foo() {
  if (std::is_integral<double>::value) {
    return std::cout << "this isn't going to work\n";
  } else {
    return 1;
  }
}

No lo hará. Aunque el optimizador puede descartar la primera rama, no podrá compilar. Aquí es donde usar enable_if y SFINAE ayuda, porque puede seleccionar el código válido (compilable), y el Fallo del código no válido (un-compilable) para compilar No Es Un Error.

 11
Author: Useless,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-05-31 13:40:23

El compilador puede ser lo suficientemente inteligente como para ver que puede reemplazar el cuerpo de la instrucción if con dos implementaciones de funciones diferentes, y simplemente elegir la correcta. Pero a partir de 2014 dudo que haya algún compilador que sea lo suficientemente inteligente para hacer eso. Puede que me equivoque. Pensándolo bien, std::is_integrales lo suficientemente simple como para pensar que será optimizado.

Su idea de sobrecargar el resultado de std::is_integral es una posible solución.

Otro y MI Humilde opinión la solución más limpia es usar std::enable_if (junto con std::is_integral).

 2
Author: Cheers and hth. - Alf,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-05-31 13:23:54

Crédito a @MooingDuck y @Casey

template<class FN1, class FN2, class ...Args>
decltype(auto) if_else_impl(std::true_type, FN1 &&fn1, FN2 &&, Args&&... args)
{
    return fn1(std::forward<Args>(args)...);
}

template<class FN1, class FN2, class ...Args>
decltype(auto) if_else_impl(std::false_type, FN1 &&, FN2 &&fn2, Args&&... args)
{
    return fn2(std::forward<Args>(args)...);
}

#define static_if(...) if_else_impl(__VA_ARGS__, *this)

Y ussage tan simple como:

static_if(do_it,
    [&](auto& self){ return 1; },
    [&](auto& self){ return self.sum(2); }
);

Funciona como estático si el compilador va solo a la rama "true".


P.d. Necesita tener self = *thisy hacer llamadas a miembros desde él, debido a error gcc. Si tiene llamadas lambda anidadas, no puede usar this-> en lugar de self.

 1
Author: tower120,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-02-24 13:49:12