¿Es más eficiente realizar una comprobación de rango fundiendo a uint en lugar de comprobar valores negativos?


Me topé con este fragmento de código en la lista de código fuente de. NET :

// Following trick can reduce the range check by one
if ((uint) index >= (uint)_size) {
  ThrowHelper.ThrowArgumentOutOfRangeException();
}

Aparentemente esto es más eficiente (?) than if (index < 0 || index >= _size)

Tengo curiosidad sobre la razón detrás del truco. ¿Es una instrucción de una sola rama realmente más cara que dos conversiones a uint? ¿O hay alguna otra optimización que hará que este código sea más rápido que una comparación numérica adicional?

Para abordar el elefante en la habitación: sí, esto es micro optimización, no, no tengo la intención de usar esto en todas partes en mi código, solo tengo curiosidad.;)

Author: enzi, 2015-03-30

7 answers

Desde MS Partition I , sección 12.1 (Tipos de datos soportados):

Los tipos enteros con signo (int8, int16, int32, int64 e int nativo) y sus correspondientes sin signo tipos enteros (int8 sin signo, int16 sin signo, int32 sin signo, int64 sin signo y nativo sin signo int) difieren solo en cómo se interpretan los bits del entero. Para aquellas operaciones en las que un entero sin signo se trata de manera diferente a un entero con signo (por ejemplo, en comparaciones o aritmética con desbordamiento) hay separados instrucciones para tratar un entero como unsigned (e.g., cgt.onu y add.ovf.Naciones).

Es decir, la conversión de un int a un uint es simplemente una cuestión de contabilidad-a partir de ahora, el valor en la pila/en un registro ahora se sabe que es un int sin signo en lugar de un int.

Por lo tanto, las dos conversiones deben ser "libres" una vez que se envía el código, y luego se puede realizar la operación de comparación sin signo.

 55
Author: Damien_The_Unbeliever,
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
2015-03-31 13:55:36

Digamos que tenemos: {[15]]}

public void TestIndex1(int index)
{
  if(index < 0 || index >= _size)
    ThrowHelper.ThrowArgumentOutOfRangeException();
}
public void TestIndex2(int index)
{
  if((uint)index >= (uint)_size)
    ThrowHelper.ThrowArgumentOutOfRangeException();
}

Compilemos estos y veamos ILSpy: {[15]]}

.method public hidebysig 
    instance void TestIndex1 (
        int32 index
    ) cil managed 
{
    IL_0000: ldarg.1
    IL_0001: ldc.i4.0
    IL_0002: blt.s IL_000d
    IL_0004: ldarg.1
    IL_0005: ldarg.0
    IL_0006: ldfld int32 TempTest.TestClass::_size
    IL_000b: bge.s IL_0012
    IL_000d: call void TempTest.ThrowHelper::ThrowArgumentOutOfRangeException()
    IL_0012: ret
}

.method public hidebysig 
    instance void TestIndex2 (
        int32 index
    ) cil managed 
{
    IL_0000: ldarg.1
    IL_0001: ldarg.0
    IL_0002: ldfld int32 TempTest.TestClass::_size
    IL_0007: blt.un.s IL_000e
    IL_0009: call void TempTest.ThrowHelper::ThrowArgumentOutOfRangeException()
    IL_000e: ret
}

Es fácil ver que el segundo tiene menos código, con una rama menos.

Realmente, no hay reparto en absoluto, existe la opción de usar blt.s y bge.s o usar blt.s.un, donde este último trata los enteros pasados como sin signo mientras que el primero los trata como firmados.

(Nota para aquellos que no están familiarizados con CIL, ya que esta es una pregunta de C# con una respuesta de CIL, bge.s, blt.s y blt.s.un son las versiones "cortas" de bge, blt y blt.un respectivamente. blt pops dos valores de la pila y ramas si el primero es menor que el segundo al considerarlos como valores con signo, mientras que blt.un cop de dos valores de la pila de ramas y si el primero es menor que el segundo al considerarlos como valores sin signo).

Es absolutamente un micro-opt, pero hay momentos en que vale la pena hacer micro-opt. Considere además, que con el resto del código en el cuerpo del método esto podría significar la diferencia entre algo que cae dentro de los límites de nerviosismo para inlining o no, y si se están molestando en tener un ayudante para lanzar fuera de rango excepciones que probablemente están tratando de asegurar que inlining sucede si es posible, y los 4 bytes adicionales podrían hacer toda la diferencia.

De hecho, es muy probable que esa diferencia de inserción sea mucho mayor que la reducción de una rama. No hay muchas veces en las que salir de su camino a asegurarse de que ocurre la inserción vale la pena, pero un método central de una clase de uso tan pesado como List<T> sería sin duda uno de ellos.

 29
Author: Jon Hanna,
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
2015-06-11 09:26:15

Tenga en cuenta que este truco no funcionará si su proyecto es checked en lugar de unchecked. En el mejor de los casos será más lento (porque cada lanzamiento tendrá que ser comprobado contra el desbordamiento) (o al menos no-más rápido), en el peor de los casos obtendrá un OverflowException si intenta pasar -1 como el index (en lugar de su excepción).

Si desea escribirlo " correctamente "y de una manera más" seguramente funcionará", debe poner un

unchecked
{
    // test
}

Alrededor de la prueba.

 8
Author: xanatos,
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
2015-03-30 11:31:47

Asumiendo _size es un entero, privado a la lista y index es el argumento a esta función cuya validez necesita ser probada.

Asumiendo además que _size es siempre >= 0.

Entonces la prueba original habría sido:

if(index < 0 || index > size) throw exception

La versión optimizada

if((uint)index > (uint)_size) throw exception

Tiene una comparación (como opsed a dos int él ejemplo anterior.) Debido a que el cast solo reinterpreta los bits y hace que el > de hecho una comparación sin signo, no hay ciclos de CPU adicionales se utilizan para ello.

Por Qué funciona?

Los resultados son simples/triviales siempre y cuando index >= 0.

Si el índice (uint)index lo convertirá en un número muy grande:

Ejemplo: 0xFFFF es -1 como int, pero 65535 como uint, por lo tanto

(uint)-1 > (uint)x 

Siempre es cierto si x era positivo.

 8
Author: DrKoch,
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
2015-03-30 12:17:01

Sí, esto es más eficiente. El JIT hace el mismo truco cuando la matriz de comprobación de rango accede a.

La transformación y el razonamiento es como sigue:

i >= 0 && i < array.Length se convierte en (uint)i < (uint)array.Length porque array.Length <= int.MaxValue de modo que array.Length tiene el mismo valor que (uint)array.Length. Si i resulta ser negativo entonces (uint)i > int.MaxValue y la comprobación falla.

 5
Author: usr,
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
2015-03-30 20:08:22

Aparentemente en la vida real no es más rápido. Compruebe esto: https://dotnetfiddle.net/lZKHmn

Resulta que, gracias a la predicción de ramas de Intel y la ejecución paralela, el código más obvio y legible realmente funciona más rápido...

Aquí está el código:

using System;
using System.Diagnostics;

public class Program
{


    const int MAX_ITERATIONS = 10000000;
    const int MAX_SIZE = 1000;


    public static void Main()
    {

            var timer = new Stopwatch();


            Random rand = new Random();
            long InRange = 0;
            long OutOfRange = 0;

            timer.Start();
            for ( int i = 0; i < MAX_ITERATIONS; i++ ) {
                var x = rand.Next( MAX_SIZE * 2 ) - MAX_SIZE;
                if ( x < 0 || x > MAX_SIZE ) {
                    OutOfRange++;
                } else {
                    InRange++;
                }
            }
            timer.Stop();

            Console.WriteLine( "Comparision 1: " + InRange + "/" + OutOfRange + ", elapsed: " + timer.ElapsedMilliseconds + "ms" );


            rand = new Random();
            InRange = 0;
            OutOfRange = 0;

            timer.Reset();
            timer.Start();
            for ( int i = 0; i < MAX_ITERATIONS; i++ ) {
                var x = rand.Next( MAX_SIZE * 2 ) - MAX_SIZE;
                if ( (uint) x > (uint) MAX_SIZE ) {
                    OutOfRange++;
                } else {
                    InRange++;
                }
            }
            timer.Stop();

            Console.WriteLine( "Comparision 2: " + InRange + "/" + OutOfRange + ", elapsed: " + timer.ElapsedMilliseconds + "ms" );

    }
}
 4
Author: nsimeonov,
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
2015-03-31 21:00:46

Mientras exploraba esto en un procesador intel, no encontré diferencias en los tiempos de ejecución, posiblemente debido a múltiples unidades de ejecución enteras.

Pero al hacer esto en un microprocesador en tiempo real de 16MHZ sin predicción de ramas ni unidades de ejecución de enteros, hubo diferencias notables.

1 millón de iteraciones del código más lento tomaron 1761 ms

int slower(char *a, long i)
{
  if (i < 0 || i >= 10)
    return 0;

  return a[i];
}

1 millón de iteraciones más rápido el código tomó 1635 ms

int faster(char *a, long i)
{
  if ((unsigned int)i >= 10)
    return 0;
  return a[i];
}
 1
Author: Serve Laurijssen,
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
2015-05-30 06:19:18