Hay muchas preguntas similares sobre SO, pero todavía tengo que encontrar una que funcione y sea fácil de trasladar a C#. La mayoría involucra C++ o similar, y las respuestas (presumiblemente) funcionales se basan en el ensamblado integrado o en funciones nativas de C/C++ que no existen en C#. Varias funciones funcionan para parte del rango, pero fallan en otras partes. Encontré una respuesta funcional que pude trasladar a C#, pero era muy lenta (resulta que es decentemente rápida cuando compilo en x64 en lugar de x86, así que la publiqué como la respuesta a vencer).
En mi caso, tengo un Int64/UInt64 aleatorio de 64 bits (usando el algoritmo xoshiro256p, aunque probablemente sea irrelevante). Quiero escalar ese número a cualquier rango arbitrario en los valores permitidos del tipo. Por ejemplo, podría querer escalar Int64 al rango [1000, 35000]. Esto es, conceptualmente, bastante fácil:
UInt64 minVal = 1000; UInt64 maxVal = 35000; UInt64 maxInt = UInt64.MaxValue; UInt64 randInt = NextUInt64(); // Random value between 0 and maxInt. UInt64 diff = maxVal - minVal + 1; UInt64 scaledInt = randInt * diff / maxInt; // This line can overflow. return scaledInt + minVal; Como lo señalaron muchas otras personas, y el comentario anterior, el problema es que randInt * diff puede potencialmente desbordarse.
En papel, podría simplemente almacenar ese resultado intermedio en un número entero de 128 bits y luego almacenar el resultado de la división en la salida de 64 bits. Pero las matemáticas de 128 bits no son nativas de los sistemas de 64 bits, y prefiero evitar las bibliotecas de precisión arbitraria, ya que haré muchas llamadas a esta función y la eficiencia será notable.
Podría multiplicar por un doble para obtener 53 bits de precisión, lo cual está bien para lo que estoy haciendo actualmente, pero prefiero encontrar una solución adecuada.
Podría crear una biblioteca de C++ con una de las soluciones de ASM y llamar a esa biblioteca, pero me gustaría algo que sea C# puro.
randInt * diff / maxInt esté en el rango [0, maxInt] (y cada valor en sí esté en el mismo rango).He probado las siguientes soluciones para el rendimiento relativo. Cada prueba ejecutó 1 millón de iteraciones de mi generador de números aleatorios, escalando usando varios métodos. Empecé generando números aleatorios y colocándolos en listas (una para firmada, otra para no firmada). Luego revisé cada lista y la escalé a una segunda lista.
Inicialmente tuve un montón de pruebas en modo de depuración. En general, no importó (estamos probando el rendimiento relativo ), pero a las bibliotecas Int128/UInt128 les fue mucho mejor en el modo de lanzamiento.
Los números entre paréntesis son el tiempo de depuración. Los incluyo aquí porque todavía quiero un rendimiento decente durante la depuración. La biblioteca Int128, por ejemplo, es excelente para el modo de lanzamiento, pero terrible para la depuración. Puede ser útil usar algo que tenga un mejor equilibrio hasta que esté listo para el lanzamiento final. Debido a que estoy probando un millón de muestras, el tiempo en milisegundos también es el tiempo en nanosegundos por operación (todos los millones de UInt64 se generan en 33 ms, por lo que cada uno se genera en 33 ns).
El código fuente para mis pruebas se puede encontrar aquí, en GitGub .
No pude hacer que estos dieran los resultados adecuados.
¿La forma más precisa de hacer una operación combinada de multiplicar y dividir en 64 bits?
¿Cómo puedo descalcificar x por n/d, cuando x*n se desborda?
Método rápido para multiplicar enteros por fracciones propias sin flotadores ni desbordamiento
(a * b) / c MulDiv y cómo lidiar con el desbordamiento de la multiplicación intermedia
¿Cómo multiplicar un entero de 64 bits por una fracción en C++ mientras se minimiza el error?
Pero las matemáticas de 128 bits no son nativas de los sistemas de 64 bits
Si bien eso es mayormente cierto, hay una manera decente de obtener el producto completo de 128 bits de dos números enteros de 64 bits: Math.BigMul (para .NET 5 y posterior)
x64 tiene una división correspondiente con una entrada de 128 bits, y tal par de multiplicaciones completas seguidas de una división amplia implementaría esta operación de "escala de enteros por una fracción propia" (con la limitación de que la fracción no debe ser mayor que 1, de lo contrario podría producirse un desbordamiento). Sin embargo, C# no tiene acceso a la división amplia y, aunque lo tuviera, no sería muy eficiente en la mayoría del hardware.
Pero también puede usar BigMul directamente, porque el divisor realmente debería ser 2 64 para empezar (no 2 64 - 1), y BigMul divide automáticamente entre 2 64 .
Entonces el código se convierte en: (no probado)
ulong ignore; ulong scaled = Math.BigMul(randInt, diff, out ignore); return scaled + minVal;Para versiones anteriores de .NET, obtener los 64 bits altos del producto podría hacerse de esta manera:
static ulong High64BitsOfProduct(ulong a, ulong b) { // decompose into 32bit blocks (in ulong to avoid casts later) ulong al = (uint)a; ulong ah = a >> 32; ulong bl = (uint)b; ulong bh = b >> 32; // low times low and high times high ulong l = al * bl; ulong h = ah * bh; // cross terms ulong x1 = al * bh; ulong x2 = ah * bl; // carry from low half of product into high half ulong carry = ((l >> 32) + (uint)x1 + (uint)x2) >> 32; // add up all the parts return h + (x1 >> 32) + (x2 >> 32) + carry; } Desafortunadamente, eso no es tan bueno como Math.BigMul , pero al menos todavía no hay división.