2010-04-06 24 views
6

Estoy leyendo un archivo de texto que contiene números en el rango [1, 10^100]. Entonces estoy realizando una secuencia de operaciones aritméticas en cada número. Me gustaría usar un BigInteger solo si el número está fuera del rango int/largo. Un enfoque sería contar cuántos dígitos hay en la cadena y cambiar a BigInteger si hay demasiados. De lo contrario, usaría aritmética primitiva ya que es más rápido. ¿Hay una mejor manera?Cambiar a BigInteger si es necesario

¿Hay alguna razón por la cual Java no podría hacer esto automáticamente, es decir, cambiar a BigInteger si un int era demasiado pequeño? De esta forma no tendríamos que preocuparnos por los desbordamientos.

Respuesta

6

Sospecho que la decisión de utilizar valores primitivos para enteros y reales (hechos por motivos de rendimiento) hizo que esa opción no sea posible. Tenga en cuenta que Python y Ruby hacen lo que usted pide.

En este caso puede ser más trabajo manejar la caja especial más pequeña de lo que vale (necesita una clase personalizada para manejar las dos cajas), y solo debe usar BigInteger.

+3

(O usa Python, Ruby, Jython o JRuby) –

+0

(-1) Está más relacionado con el tipado estático frente al tipado dinámico, en lugar de primitivo frente a no primitivo. – ewernli

+1

@ewemli Con el tipado estático todavía podrías hacer el cambio dentro de una clase contenedora (usando subtipos ocultos), pero estoy de acuerdo en que el tipado dinámico lo hace más fácil. Tener primitivos niega la capacidad de usar una clase contenedora. –

4

¿Hay alguna razón por la cual Java no podría hacer esto automáticamente, es decir, cambiar a BigInteger si un int era demasiado pequeño?

Porque ese es un comportamiento de programación de nivel superior al que Java tiene actualmente. El lenguaje ni siquiera es consciente de la clase BigInteger y de lo que hace (es decir, no está en JLS). Solo está al tanto de Integer (entre otras cosas) para boxeo y desempaquetar.

Hablando de boxeo/unboxing, un int es un tipo primitivo; BigInteger es un tipo de referencia. No puede tener una variable que pueda contener valores de ambos tipos.

1

Puede leer los valores en BigInteger s, y luego convertirlos a long s si son lo suficientemente pequeños.

private final BigInteger LONG_MAX = BigInteger.valueOf(Long.MAX_VALUE); 
private static List<BigInteger> readAndProcess(BufferedReader rd) throws IOException { 
    List<BigInteger> result = new ArrayList<BigInteger>(); 
    for (String line; (line = rd.readLine()) != null;) { 
     BigInteger bignum = new BigInteger(line); 
     if (bignum.compareTo(LONG_MAX) > 0) // doesn't fit in a long 
      result.add(bignumCalculation(bignum)); 
     else result.add(BigInteger.valueOf(primitiveCalculation(bignum.longValue()))); 
    } 
    return result; 
} 
private BigInteger bignumCalculation(BigInteger value) { 
    // perform the calculation 
} 
private long primitiveCalculation(long value) { 
    // perform the calculation 
} 

(Usted podría hacer que el valor de retorno de una List<Number> y tienen una colección mixta de BigInteger y Long objetos, pero que no se vería muy agradable y no mejoraría el rendimiento por una gran cantidad.)

El rendimiento puede mejor si una gran cantidad de los números en el archivo son lo suficientemente pequeños como para caber en un long (dependiendo de la complejidad del cálculo). Todavía existe riesgo de desbordamiento dependiendo de lo que haga en primitiveCalculation, y ahora ha repetido el código, (al menos) doblando el potencial de error, por lo que tendrá que decidir si la ganancia de rendimiento realmente lo vale.

Sin embargo, si su código se parece a mi ejemplo, probablemente tenga más que ganar paralelizando el código para que los cálculos y las E/S no se realicen en el mismo subproceso. Tendría que hacer algunos bastante pesados ​​ cálculos para una arquitectura como la de ser CPU-bound.

+0

Buen comentario. @fahdshariff, en realidad debería comparar A (MAX_VALUE_THAT_WILL_NOT_OVERFLOW) – tucuxi

-2

¿Hay alguna razón por la que Java no podía hacer este interruptor de forma automática, es decir a BigInteger si un int era demasiado pequeño?

Ésta es una de las ventajas de tipado dinámico, pero Java es tipos estáticos y evita esto.

En un lenguaje de tipo dinámico cuando dos Integer que se suman producirían un desbordamiento, el sistema puede devolver, por ejemplo, un Long. Debido a que el lenguaje de tipado dinámico depende de la tipificación de pato, está bien. Lo mismo no puede suceder en un lenguaje estáticamente tipado; rompería el sistema de tipos.

EDITAR

Teniendo en cuenta que mi respuesta y comentario no estaba claro, aquí trato de dar más detalles por eso que creo que los tipos estáticos es el tema principal:

1) el mismo hecho que hablamos del tipo primitivo es un problema de tipado estático; no nos importaría en un lenguaje de tipo dinámico.

2) con los tipos primitivos, el resultado de la desbordamiento no se puede convertir a otro tipo de una int porque no sería correcto estática wrt escribiendo

int i = Integer.MAX_VALUE + 1; // -2147483648 

3) con los tipos de referencia, que es la misma excepto que tenemos autoboxing. Aún así, la adición no podría devolver, por ejemplo, un BigInteger porque no coincidiría con el sistema de tipo estático (A BigInteger no se puede convertir a Integer).

Integer j = new Integer(Integer.MAX_VALUE) + 1; // -2147483648 

4) lo que podría hacerse es una subclase, por ejemplo, Number y poner en práctica en casa Tipo UnboundedNumeric que optimiza la representación interna (independencia de representación).

UnboundedNum k = new UnboundedNum(Integer.MAX_VALUE).add(1); // 2147483648 

Aún así, no es realmente la respuesta a la pregunta original.

5) con tipado dinámico, algo así como

var d = new Integer(Integer.MAX_VALUE) + 1; // 2147483648 

devolvería un Long que está bien.

+0

Eso no es realmente relevante para el tipeo dinámico/estático. BigInteger/int es una cuestión de representación. Hubiera sido posible para Java manejar esto, tener dos representaciones para el mismo tipo, aunque sería algo complicado. –

+0

Daniel tiene razón, escribir no es el problema. – Jakob

+0

@Daniel La misma oración "tener dos representaciones para el mismo tipo" implica que podríamos agregar un nuevo tipo que tiene una representación dual. El punto es que este * nuevo * tipo de envoltura es necesario solo porque está * estáticamente * escrito. Lo que quiero decir es que con el tipado dinámico no es necesario agregar ningún tipo nuevo, está * trivialmente * resuelto. Lo cual creo que hace la distinción dinámica/estática * muy * relevante en este contexto; lo que estamos discutiendo aquí se sabe que es una ventaja del tipado dinámico. – ewernli

1

El impacto del uso de BigDecimals cuando algo más pequeño será suficiente es sorprendente, err, grande: Ejecutar el siguiente código

public static class MyLong { 
    private long l; 
    public MyLong(long l) { this.l = l; } 
    public void add(MyLong l2) { l += l2.l; } 
} 

public static void main(String[] args) throws Exception { 
    // generate lots of random numbers 
    long ls[] = new long[100000]; 
    BigDecimal bds[] = new BigDecimal[100000]; 
    MyLong mls[] = new MyLong[100000]; 
    Random r = new Random(); 
    for (int i=0; i<ls.length; i++) { 
     long n = r.nextLong(); 
     ls[i] = n; 
     bds[i] = new BigDecimal(n); 
     mls[i] = new MyLong(n); 
    } 
    // time with longs & Bigints 
    long t0 = System.currentTimeMillis(); 
    for (int j=0; j<1000; j++) for (int i=0; i<ls.length-1; i++) { 
     ls[i] += ls[i+1]; 
    } 
    long t1 = Math.max(t0 + 1, System.currentTimeMillis()); 
    for (int j=0; j<1000; j++) for (int i=0; i<ls.length-1; i++) { 
     bds[i].add(bds[i+1]); 
    } 
    long t2 = System.currentTimeMillis(); 
    for (int j=0; j<1000; j++) for (int i=0; i<ls.length-1; i++) { 
     mls[i].add(mls[i+1]); 
    } 
    long t3 = System.currentTimeMillis(); 
    // compare times 
    t3 -= t2; 
    t2 -= t1; 
    t1 -= t0; 
    DecimalFormat df = new DecimalFormat("0.00"); 
    System.err.println("long: " + t1 + "ms, bigd: " + t2 + "ms, x" 
      + df.format(t2*1.0/t1) + " more, mylong: " + t3 + "ms, x" 
      + df.format(t3*1.0/t1) + " more"); 
} 

produce, en mi sistema, esta salida:

largos: 375ms , BigD: 6296ms, x16.79 más, mylong: 516ms, x1.38 más

La clase MyLong hay más que mirar a los efectos del cuadro ing, para comparar con lo que obtendrías con una clase personalizada BigOrLong.

+0

¿Por qué usar 'BigDecimal' si' BigInt' sería suficiente? Un tipo no entero es naturalmente mucho más lento. –

+0

Es cierto, pero después de sustituir 'BigInteger' por' BigDecimal' no hay mucha diferencia: 'long: 375ms, bigi: 5843ms, x15.58 más, mylong: 532ms, x1.42 más' – tucuxi

0

¿Hubiera sido posible? Sí. Pero hay muchos problemas con eso.

Considérese, por ejemplo, que almacena Java referencias a BigInteger, que en realidad está asignada en el montón, pero almacenar int literales. La diferencia puede ser dejado claro en C:

int i; 
BigInt* bi; 

Ahora, para ir automáticamente a partir de un literal con una referencia, uno necesariamente tiene que realizar anotaciones en el literal de alguna manera. Por ejemplo, si se estableció el bit más alto de la int, entonces los otros bits podrían usarse como una búsqueda de tabla de algún tipo para recuperar la referencia adecuada. Eso también significa que obtendrá un BigInt** bi cada vez que se desborde en eso.

Por supuesto, ese es el bit que generalmente se utiliza para firmar, y las instrucciones de hardware dependen bastante de ello. Peor aún, si hacemos eso, entonces el hardware no podrá detectar el desbordamiento y configurará los indicadores para indicarlo. Como resultado, cada operación debería ir acompañada de alguna prueba para ver si el desbordamiento ha ocurrido o sucederá (dependiendo de cuándo se puede detectar).

Todo eso agregaría una gran cantidad de sobrecarga a la aritmética de enteros básicos, lo que en la práctica anularía los beneficios que tenía que comenzar. En otras palabras, es más rápido asumir que BigInt que tratar de usar int y detectar condiciones de desbordamiento, al mismo tiempo que se hace malabares con el problema de referencia/literal.

Por lo tanto, para obtener una ventaja real, uno tendría que usar más espacio para representar las entradas. Entonces, en lugar de almacenar 32 bits en la pila, en los objetos o en cualquier otro lugar donde los usemos, almacenamos 64 bits, por ejemplo, y usamos los 32 bits adicionales para controlar si queremos una referencia o un literal. Eso podría funcionar, pero hay un problema obvio: uso del espacio. :-) Sin embargo, podríamos ver más con hardware de 64 bits.

Ahora, puede preguntar por qué no solo 40 bits (32 bits + 1 byte) en lugar de 64? Básicamente, en el hardware moderno es preferible almacenar cosas en incrementos de 32 bits por razones de rendimiento, por lo que de todos modos, se rellenarán 40 bits a 64 bits.

EDITAR

Vamos a considerar cómo se puede ir haciendo esto en C#. Ahora, no tengo experiencia en programación con C#, así que no puedo escribir el código para hacerlo, pero espero poder dar una visión general.

La idea es crear una estructura para ello. Debe tener un aspecto más o menos así:

public struct MixedInt 
{ 
    private int i; 
    private System.Numeric.BigInteger bi; 

    public MixedInt(string s) 
    { 
     bi = BigInteger.Parse(s); 
     if (parsed <= int.MaxValue && parsed => int.MinValue) 
     { 
      i = (int32) parsed; 
      bi = 0; 
     } 
    } 

    // Define all required operations 
} 

lo tanto, si el número está en el rango de números enteros que utilizamos int, de lo contrario utilizamos BigInteger. Las operaciones deben garantizar la transición de uno a otro según sea necesario/posible. Desde el punto de vista del cliente, esto es transparente. Es solo un tipo MixedInt, y la clase se ocupa de usar lo que mejor se adapte.

Tenga en cuenta, sin embargo, que este tipo de optimización bien puede ser parte de C# BigInteger ya, dada su implementación como una estructura.

Si Java tuviera algo así como la estructura de C#, podríamos hacer algo como esto en Java también.

+0

Si entiendo bien lo que quiere decir, un 'int i' en el código podría ocultar un' BigInt' porque el tiempo de ejecución habría cambiado la representación. Para mí, esto significa que un 'int' no es más un' int' w.r.t para el sistema de tipo, porque su límite (tipo) ya no se aplica.Pero sería posible agregar otro tipo, por ejemplo, 'ilimitado' que haga lo que describa y optimice la representación en tiempo de ejecución. Pero eso es algo completamente diferente. – ewernli

+0

@ewernli No, eso no es lo que quiero decir en absoluto. El sistema de tipos no tiene nada que ver con eso. Digamos, por ejemplo, que quería implementar algo así en Scala. Simplemente lo llamaría [Int, BigInt] y definiría las operaciones sobre él. La diferencia es una entre una referencia, un puntero y un literal. Un literal se almacena como en todas partes: registros, pila, montón. Una referencia se almacena como un puntero al lugar en el montón que contiene los datos. Cuando miras un dato X, ¿cómo sabes si es una referencia o un literal? Ese es el problema que es difícil de resolver. Ver respuesta editada. –

+0

Su explicación en realidad muestra que * es * un problema de tipeo estático: o bien necesita (1) una extensión del sistema de tipo con 'Cualquiera de [...]' o lo que sea (2) para definir un tipo de envoltura artificial que optimiza la representación internamente, pero se presenta solo para resolver este problema de tipado estático (como su ejemplo de C#). Con tipeo dinámico, nada de eso es necesario y el problema está * trivialmente * resuelto; lo que quise decir es que lo principal que le impide * trivialmente * hacer eso * es * el * sistema * actual tipo java. – ewernli

1

Java es rápido, realmente muy rápido. Es solo 2-4 veces más lento que c y, a veces, tan rápido o un poco más rápido donde la mayoría de los otros idiomas son 10x (python) a 100x (ruby) más lento que C/Java.(Fortran también es hella-fast, por cierto)

Parte de esto se debe a que no hace cosas como cambiar los tipos de número para usted. Podría, pero actualmente puede alinear una operación como "a * 5" en unos pocos bytes, imagine los aros que tendría que atravesar si a fuera un objeto. Sería al menos una llamada dinámica al método de multiplicación de un, que sería unos pocos cientos/mil veces más lento de lo que era cuando a era simplemente un valor entero.

Probablemente, Java podría actualmente utilizar la compilación JIT para optimizar la llamada mejor y alinearla en tiempo de ejecución, pero incluso entonces muy pocas llamadas de biblioteca admiten BigInteger/BigDecimal por lo que habría MUCHA compatibilidad nativa, sería un lenguaje completamente nuevo.

¡También imagine cómo cambiar de int a BigInteger en lugar de a lo largo haría que los videojuegos de depuración fuesen duros! (Sí, cada vez que nos movemos hacia el lado derecho de la pantalla, el juego se ralentiza 50 veces, ¡el código es el mismo! ¿Cómo es posible?!?)

Cuestiones relacionadas