Rust: Las conversiones de punto flotante a entero pueden causar un comportamiento indefinido

Creado en 31 oct. 2013  ·  234Comentarios  ·  Fuente: rust-lang/rust

Estado al 18-04-2020

Tenemos la intención de estabilizar el comportamiento saturating-float-casts para as , y hemos estabilizado funciones de biblioteca inseguras que manejan el comportamiento anterior. Consulte el n. ° 71269 para ver la discusión más reciente sobre ese proceso de estabilización.

Estado al 05/11/2018

Se ha implementado una bandera en el compilador, -Zsaturating-float-casts , que hará que todas las conversiones flotantes a enteras tengan un comportamiento de "saturación" donde, si está fuera de los límites, se fija al límite más cercano. Hace un tiempo se hizo un llamado a la evaluación comparativa de este cambio. Los resultados, aunque positivos en muchos proyectos, son bastante negativos para algunos proyectos e indican que no hemos terminado aquí.

Los siguientes pasos consisten en descubrir cómo recuperar el rendimiento en estos casos:

  • Una opción es tomar el comportamiento de conversión actual as (que es UB en algunos casos) y agregar funciones unsafe para los tipos relevantes y demás.
  • Otra es esperar a que LLVM agregue un concepto freeze que significa que obtenemos un patrón de bits de basura, pero al menos no es UB
  • Otro es implementar conversiones a través de ensamblaje en línea en LLVM IR, ya que el codegen actual no está muy optimizado.

Estado antiguo

ACTUALIZACIÓN (por @nikomatsakis): Después de mucha discusión, tenemos los rudimentos de un plan sobre cómo abordar este problema. ¡Pero necesitamos ayuda para investigar realmente el impacto en el rendimiento y resolver los detalles finales!


EL NÚMERO ORIGINAL SIGUE:

Si el valor no cabe en ty2, los resultados no están definidos.

1.04E+17 as u8
A-LLVM C-bug I-unsound 💥 P-medium T-lang

Comentario más útil

Comencé a trabajar para implementar intrínsecos para saturar flotadores a int casts en LLVM: https://reviews.llvm.org/D54749

Si eso va a alguna parte, proporcionará una forma relativamente baja de obtener la semántica de saturación.

Todos 234 comentarios

Nominando

aceptado para P-high, el mismo razonamiento que # 10183

No creo que esto sea incompatible al revés a nivel de idioma. No hará que el código que estaba funcionando bien deje de funcionar. Nominación.

cambiando a P-high, el mismo razonamiento que # 10183

¿Cómo nos proponemos solucionar esto y # 10185? Dado que si el comportamiento está definido o no depende del valor dinámico del número que se está emitiendo, parece que la única solución es insertar controles dinámicos. Parece que estamos de acuerdo en que no queremos hacer eso por desbordamiento aritmético, ¿estamos felices de hacerlo por desbordamiento del reparto?

Podríamos agregar un intrínseco a LLVM que realiza una "conversión segura". @zwarich puede tener otras ideas.

AFAIK, la única solución en este momento es utilizar los intrínsecos específicos del objetivo. Eso es lo que hace JavaScriptCore, al menos según alguien a quien le pregunté.

Oh, eso es bastante fácil entonces.

ping @pnkfelix ¿ está esto cubierto por el nuevo material de verificación de desbordamiento?

Rustc no comprueba estas conversiones con aserciones de depuración.

Estoy feliz de manejar esto, pero necesito una solución concreta. Personalmente, creo que debería comprobarse junto con la aritmética de números enteros desbordantes, ya que es un problema muy similar. Aunque realmente no me importa lo que hagamos.

Tenga en cuenta que este problema actualmente está causando un ICE cuando se usa en ciertas expresiones constantes.

Esto permite violar la seguridad de la memoria en safe rust, ejemplo de esta publicación en el foro :

Undefs, ¿eh? Los undef son divertidos. Tienden a propagarse. Después de unos minutos de peleas ...

#[inline(never)]
pub fn f(ary: &[u8; 5]) -> &[u8] {
    let idx = 1e100f64 as usize;
    &ary[idx..]
}

fn main() {
    println!("{}", f(&[1; 5])[0xdeadbeef]);
}

segfaults en mi sistema (la última noche) con -O.

Marcar con I-unsound dada la violación de la seguridad de la memoria en el óxido seguro.

@bluss , esto no se segmenta para mí, solo da un error de afirmación. desetiquetar ya que fui yo quien lo agregó

Suspiro, olvidé la -O, volver a etiquetar.

re-nominación para P-high. Aparentemente, esto fue en algún momento P-alto pero bajó con el tiempo. Esto parece bastante importante para la corrección.

EDITAR: no reaccionó al comentario de clasificación, agregando la etiqueta manualmente.

Parece que el precedente de las cosas de desbordamiento (por ejemplo, para cambiar) es simplemente asentarse en algún comportamiento. Java parece producir el resultado módulo el rango, lo que no parece irrazonable; No estoy seguro de qué tipo de código LLVM necesitaríamos para manejar eso.

Según https://docs.oracle.com/javase/specs/jls/se7/html/jls-5.html#jls -5.1.3 Java también garantiza que los valores de NaN se asignan a 0 e infinitos al número entero mínimo / máximo representable. Además, la regla de Java para la conversión es más compleja que la simple envoltura, puede ser una combinación de saturación (para la conversión a int o long ) y envoltura (para la conversión a tipos integrales más pequeños , si es necesario). Sin duda, es posible replicar todo el algoritmo de conversión de Java, pero requeriría una gran cantidad de operaciones para cada conversión. En particular, para asegurar que el resultado de una operación fpto[us]i en LLVM no exhiba un comportamiento indefinido, se necesitaría una verificación de rango.

Como alternativa, sugeriría que se garantiza que las conversiones float-> int solo sean válidas si el truncamiento del valor original se puede representar como un valor del tipo de destino (o tal vez como [iu]size ?) Y para tener aserciones en compilaciones de depuración que provocan pánico cuando el valor no se ha representado fielmente.

Las principales ventajas del enfoque de Java son que la función de conversión es total, pero esto también significa que puede aparecer un comportamiento inesperado: evitaría un comportamiento indefinido, pero sería fácil engañarlo para que no compruebe si el reparto realmente tiene algún sentido. (Desafortunadamente, esto también es cierto para los otros elencos: preocupado :).

El otro enfoque coincide con el que se usa actualmente para las operaciones aritméticas: implementación simple y eficiente en la versión, pánicos provocados por la verificación de rango en la depuración. Desafortunadamente, a diferencia de otras conversiones de as , esto haría que dicha conversión esté marcada, lo que puede ser sorprendente para el usuario (aunque tal vez la analogía con las operaciones aritméticas pueda ayudar aquí). Esto también rompería algún código, pero AFAICT, solo debería suceder para el código que actualmente se basa en un comportamiento indefinido (es decir, reemplazaría el comportamiento indefinido "devolvamos cualquier número entero, obviamente no te importa cuál" con un pánico).

El problema no es "devolvamos un número entero, obviamente no te importa cuál", es que causa un valor indefinido que no es un valor aleatorio, sino un valor de demonio nasal y LLVM puede asumir que el valor indefinido nunca ocurre. habilitando optimizaciones que hacen cosas horribles incorrectas. Si fuera un valor aleatorio, pero fundamentalmente no indefinido, sería suficiente para solucionar los problemas de solidez. No necesitamos definir cómo se representan los valores no representables, solo necesitamos prevenir undef.

Discutido en la reunión del compilador @ rust-lang /. El curso de acción más consistente sigue siendo:

  1. cuando las verificaciones de desbordamiento están habilitadas, verifique si hay lanzamientos ilegales y pánico.
  2. de lo contrario, necesitamos un comportamiento alternativo, debería ser algo que tenga un costo de tiempo de ejecución mínimo (idealmente, cero) para los valores válidos, pero el comportamiento preciso no es tan importante, siempre que no sea LLVM undef.

El principal problema es que necesitamos una sugerencia concreta para la opción 2.

triaje: P-medio

@nikomatsakis ¿ as alguna vez en pánico en las compilaciones de depuración? Si no es así, por coherencia y previsibilidad, parece preferible mantenerlo así. (Creo que debería haberlo hecho, como la aritmética, pero ese es un debate pasado y separado).

de lo contrario, necesitamos un comportamiento alternativo, debería ser algo que tenga un costo de tiempo de ejecución mínimo (idealmente, cero) para los valores válidos, pero el comportamiento preciso no es tan importante, siempre que no sea LLVM undef.

Sugerencia concreta: extraer dígitos y exponente como u64 y dígitos de desplazamiento de bits por exponente.

fn f64_as_u64(f: f64) -> u64 {
    let (mantissa, exponent, _sign) = f.integer_decode();
    mantissa >> ((-exponent) & 63)
}

Sí, no es de costo cero, pero es algo optimizable (sería mejor si marcamos integer_decode inline ) y al menos determinista. Un futuro MIR-pass que expanda un float-> int cast probablemente podría analizar si el float está garantizado para lanzar y omitir esta conversión pesada.

¿LLVM no tiene elementos intrínsecos de plataforma para las funciones de conversión?

EDITAR : @zwarich dijo (hace mucho tiempo):

AFAIK, la única solución en este momento es utilizar los intrínsecos específicos del objetivo. Eso es lo que hace JavaScriptCore, al menos según alguien a quien le pregunté.

¿Por qué molestarse en entrar en pánico? AFAIK, @glaebhoerl es correcto, se supone que as trunca / extiende, _no_ verificar los operandos.

El sábado, 05 de marzo de 2016 a las 03:47:55 AM -0800, Gábor Lehel escribió:

@nikomatsakis ¿ as alguna vez en pánico en las compilaciones de depuración? Si no es así, por coherencia y previsibilidad, parece preferible mantenerlo así. (Creo que debería haberlo hecho, como la aritmética, pero ese es un debate pasado y separado).

Cierto. Encuentro eso persuasivo.

El miércoles 9 de marzo de 2016 a las 02:31:05 AM -0800, Eduard-Mihai Burtescu escribió:

¿LLVM no tiene elementos intrínsecos de plataforma para las funciones de conversión?

EDITAR :

AFAIK, la única solución en este momento es utilizar los intrínsecos específicos del objetivo. Eso es lo que hace JavaScriptCore, al menos según alguien a quien le pregunté.

¿Por qué molestarse en entrar en pánico? AFAIK, @glaebhoerl es correcto, se supone que as trunca / extiende, _no_ verificar los operandos.

Sí, creo que me equivoqué antes. as es el "truncamiento sin marcar"
operador, para bien o para mal, y parece mejor mantener la coherencia
con esa filosofía. El uso de intrínsecos específicos del objetivo puede ser una
aunque buena solución?

@nikomatsakis : ¿parece que el comportamiento aún no se ha definido? ¿Puede darnos una actualización sobre la planificación al respecto?

Me encontré con esto con números mucho más pequeños

    let x: f64 = -1.0;
    x as u8

Resultados en 0, 16, etc., dependiendo de las optimizaciones, esperaba que se definiera como 255 para no tener que escribir x as i16 as u8 .

@gmorenz ¿ !0u8 ?

En un contexto que no tendría sentido, obtenía el f64 de una transformación en los datos enviados a través de la red, con un rango de [-255, 255]. Tenía la esperanza de que se ajustara bien (de la misma manera que <i32> as u8 envuelve).

Aquí hay una propuesta reciente de LLVM para "matar undef" http://lists.llvm.org/pipermail/llvm-dev/2016-October/106182.html , aunque apenas tengo el conocimiento suficiente para saber si esto se resolvería automáticamente o no. este problema.

Están reemplazando undef con veneno, la semántica es ligeramente diferente. No va a hacer que int -> float casts tenga un comportamiento definido.

Probablemente deberíamos proporcionar alguna forma explícita de hacer un reparto saturante. Quería ese comportamiento exacto en este momento.

Parece que esto debería estar marcado I-crash, dado https://github.com/rust-lang/rust/issues/10184#issuecomment -139858153.

Tuvimos una pregunta sobre esto en #rust-beginners hoy, alguien se encontró con ella en la naturaleza.

El libro que estoy escribiendo con @jimblandy , _Programming Rust_, menciona este error.

Se permiten varios tipos de yesos.

  • Los números se pueden convertir de cualquiera de los tipos numéricos integrados a cualquier otro.

    (...)

    Sin embargo, al momento de escribir este artículo, convertir un valor de punto flotante grande a un tipo entero que es demasiado pequeño para representarlo puede generar un comportamiento indefinido. Esto puede causar accidentes incluso en Rust seguro. Es un error en el compilador, github.com/rust-lang/rust/issues/10184 .

Nuestra fecha límite para este capítulo es el 19 de mayo. Me encantaría eliminar el último párrafo, pero creo que al menos deberíamos tener algún tipo de plan aquí primero.

Aparentemente, JavaScriptCore actual utiliza un truco interesante en x86. Usan la instrucción CVTTSD2SI, luego recurren a un poco de C ++ complicado si el valor está fuera de rango. Dado que los valores fuera de rango explotan actualmente, usar esa instrucción (¡sin respaldo!) Sería una mejora de lo que tenemos ahora, aunque solo para una arquitectura.

Honestamente, creo que deberíamos desaprobar las conversiones numéricas con as y usar From y TryFrom o algo así como la caja de conv.

Tal vez sea así, pero eso me parece ortogonal.

OK, acabo de volver a leer toda esta conversación. Creo que hay un acuerdo en que esta operación no debe entrar en pánico (por coherencia general con as ). Hay dos contendientes principales para lo que debería ser el comportamiento:

  • Algún tipo de resultado definido

    • Pro: Creo que esto es lo más coherente con nuestra filosofía general hasta ahora.

    • Con: No parece haber una forma verdaderamente portátil de producir un resultado definido en particular en este caso. Esto implica que estaríamos usando intrínsecos específicos de la plataforma con algún tipo de respaldo para valores fuera de rango (por ejemplo, retroceder a la saturación , esta función que propuso @ oli-obk , a la definición de Java , o cualquier "C ++ peludo" Usos JSC .

    • En el peor de los casos, podemos simplemente insertar algunos if para los casos "fuera de rango".

  • Un valor indefinido (comportamiento no indefinido)

    • Ventaja: esto nos permite usar los elementos intrínsecos específicos de la plataforma que están disponibles en cada plataforma.

    • Con: es un peligro para la portabilidad. En general, siento que no hemos hecho uso de resultados indefinidos muy a menudo, al menos en el lenguaje (estoy seguro de que lo hacemos en las librerías en varios lugares).

No me queda claro si existe un precedente claro de cuál debería ser el resultado en el primer caso.

Después de haber escrito eso, mi preferencia sería mantener un resultado determinista. Siento que cada lugar en el que podemos mantenernos al margen del determinismo es una victoria. Sin embargo, no estoy muy seguro de cuál debería ser el resultado.

Me gusta la saturación porque puedo entenderla y parece útil, pero de alguna manera parece incongruente con la forma en que u64 as u32 hace el truncamiento. Entonces, tal vez tenga sentido algún tipo de resultado basado en el truncamiento, que supongo que probablemente sea lo que propuso @ oli-obk : no entiendo completamente qué se pretende que haga ese código. =)

Mi código da el valor correcto para cosas en el rango 0..2 ^ 64 y valores deterministas pero falsos para todo lo demás.

los flotantes están representados por mantisa ^ exponente, por ejemplo, 1.0 es (2 << 52) ^ -52 y dado que los desplazamientos de bits y los exponentes son lo mismo en binario, podemos revertir el desplazamiento (por lo tanto, la negación del exponente y la derecha cambio).

+1 para determinismo.

Veo dos semánticas que tienen sentido para los humanos, y creo que deberíamos elegir la que sea más rápida para los valores que están en el rango, cuando el compilador no puede optimizar ninguno de los cálculos . (Cuando el compilador sabe que un valor está dentro del rango, ambas opciones dan los mismos resultados, por lo que son igualmente optimizables).

  • Saturación (los valores fuera de rango se convierten en IntType::max_value() / min_value() )
  • Módulo (los valores fuera de rango se tratan como si primero se convirtieran a un bigint y luego se truncaran)

La siguiente tabla está destinada a especificar ambas opciones por completo. T es cualquier tipo de entero de máquina. Tmin y Tmax son T::min_value() y T::max_value() . RTZ (v) significa tomar el valor matemático de vy redondear hacia cero para obtener un número entero matemático.

v | v as T (saturación) | v as T (módulo)
---- | ---- | ----
en rango (Tmin <= v <= Tmax) | RTZ (v) | RTZ (v)
cero negativo | 0 | 0
NaN | 0 | 0
Infinity | Tmax | 0
-Infinity | Tmin | 0
v> Tmax | Tmax | RTZ (v) truncado para ajustarse a T
v <Tmin | Tmin | RTZ (v) truncado para ajustarse a T

El estándar ECMAScript especifica las operaciones ToInt32 , ToUint32, ToInt16, ToUint16, ToInt8, ToUint8, y mi intención con la opción "módulo" anterior es hacer coincidir esas operaciones en todos los casos.

ECMAScript también especifica ToInt8Clamp que no coincide con ninguno de los casos anteriores: "redondea la mitad a par" en valores fraccionarios en lugar de "redondear a cero".

La sugerencia de @ oli-obk es una tercera forma, que vale la pena considerar si es más rápido de calcular, para valores que están dentro del rango.

@ oli-obk ¿Qué pasa con los tipos enteros con signo?

Lanzando otra propuesta a la mezcla: Mark u128 lanza a flotadores como inseguro y obliga a la gente a elegir explícitamente una forma de manejarlo. u128 es bastante raro actualmente.

@Manishearth Espero tener semánticas similares enteros → flota como flotantes → enteros. Dado que ambos son UB-ful, y ya no podemos hacer que float → integer sea inseguro, probablemente deberíamos evitar hacer que integer → float también sea inseguro.

Para float → integer saturación será más rápido AFAICT (resultando en una secuencia de and , prueba + comparación de flotador de salto y salto, todo por 0.66 o 0.5 2-3 ciclos en arcos modernos). Personalmente, no podría importarme menos el comportamiento exacto que decidimos, siempre que los valores dentro del rango sean tan rápidos como sea posible.

¿No tendría sentido hacer que se comporte como un desbordamiento? Entonces, en una compilación de depuración, entraría en pánico si realiza un lanzamiento con comportamiento indefinido. Luego, podría tener métodos para especificar el comportamiento de transmisión como 1.04E+17.saturating_cast::<u8>() , unsafe { 1.04E+17.unsafe_cast::<u8>() } y potencialmente otros.

Oh, pensé que el problema era solo para u128, y podemos hacerlo inseguro en ambos sentidos.

@cryze UB no debería existir ni siquiera en modo de lanzamiento en código seguro. El desbordamiento sigue siendo un comportamiento definido.

Dicho esto, cunda el pánico en la depuración yen el lanzamiento sería genial.

Esto afecta:

  • f32 -> u8, u16, u32, u64, u128, usize ( -1f32 as _ para todos, f32::MAX as _ para todos menos u128)
  • f32 -> i8, i16, i32, i64, i128, isize ( f32::MAX as _ para todos)
  • f64 -> todas las entradas ( f64::MAX as _ para todas)

f32::INFINITY as u128 también es UB

@CryZe

¿No tendría sentido hacer que se comporte como un desbordamiento? Entonces, en una compilación de depuración, entraría en pánico si realiza un lanzamiento con comportamiento indefinido.

Esto es lo que pensé inicialmente, pero se me recordó que las conversiones as nunca entran en pánico en la actualidad (no hacemos verificación de desbordamiento con as , para bien o para mal). Entonces lo más análogo es que "haga algo definido".

FWIW, lo de "matar undef" proporcionaría, de hecho, una forma de arreglar la inseguridad de la memoria, pero dejando el resultado no determinista. Uno de los componentes clave es:

3) Cree una nueva instrucción, '% y = freeze% x', que detenga la propagación de
veneno. Si la entrada es veneno, entonces devuelve un arbitrario, pero fijo,
valor. (como antiguo undef, pero cada uso obtiene el mismo valor), de lo contrario
simplemente devuelve su valor de entrada.

La razón por la que las undefs se pueden usar para violar la seguridad de la memoria hoy en día es que pueden cambiar mágicamente los valores entre usos: en particular, entre una verificación de límites y la aritmética de puntero posterior. Si rustc agregara un congelamiento después de cada lanzamiento peligroso, obtendría un valor desconocido pero por lo demás de buen comportamiento. En cuanto al rendimiento, la congelación es básicamente gratuita aquí, ya que, por supuesto, la instrucción de la máquina correspondiente al reparto produce un valor único, no fluctuante; incluso si el optimizador tiene ganas de duplicar la instrucción de conversión por alguna razón, debería ser seguro hacerlo porque el resultado de las entradas fuera de rango suele ser determinista en una arquitectura determinada.

... Pero no determinista en todas las arquitecturas, si alguien se pregunta. x86 devuelve 0x80000000 para todas las entradas incorrectas; ARM se satura para entradas fuera de rango y (si estoy leyendo este pseudocódigo a la derecha) devuelve 0 para NaN. Entonces, si el objetivo es producir un resultado determinista e independiente de la plataforma , no es suficiente usar el intrínseco fp-to-int de la plataforma; al menos en ARM, también debe verificar el registro de estado para ver si hay una excepción. Esto puede tener algo de sobrecarga en sí mismo y ciertamente evita la autovectorización en el improbable caso de que el uso del intrínseco no lo haya hecho ya. Alternativamente, supongo que podría probar explícitamente los valores dentro del rango usando operaciones de comparación regulares y luego usar un float-to-int regular. Eso suena mucho mejor en el optimizador ...

as conversiones nunca entran en pánico en la actualidad

En algún momento cambiamos + a pánico (en modo de depuración). No me sorprendería ver el pánico de as en casos que anteriormente eran UB.

Si nos preocupamos por verificar (lo cual deberíamos), entonces deberíamos desaprobar as (¿hay algún caso de uso en el que sea la única buena opción?) O al menos desaconsejar su uso, y mover a las personas a cosas como TryFrom y TryInto lugar, que es lo que dijimos que planeábamos hacer cuando se decidió dejar as tal cual. No creo que los casos en discusión sean cualitativamente diferentes, en abstracto , de los casos en los que as ya está definido para no realizar ninguna verificación. La diferencia es solo que en la práctica la implementación para estos casos está actualmente incompleta y tiene UB. Un mundo en el que no se puede confiar en as haciendo comprobaciones (porque para la mayoría de tipos, no lo hace), y no se puede confiar en ella no entrar en pánico (ya que para algunos tipos, lo haría), y no es consistente, y todavía no lo hemos desaprobado, me parece el peor de todos.

Entonces, creo que en este punto @jorendorff básicamente enumeró lo que me parece el mejor plan :

  • as tendrá un comportamiento determinista;
  • Elegiremos un comportamiento basado en una combinación de cuán sensato es y cuán eficiente es.

Enumeró tres posibilidades. Creo que el trabajo restante es investigar esas posibilidades, o al menos investigar una de ellas. Es decir, impleméntelo y trate de tener una idea de lo "lento" o "rápido" que es.

¿Hay alguien por ahí que se sienta motivado para intentarlo? Voy a etiquetar esto como E-help-wanted con la esperanza de atraer a una persona. (@ oli-obk?)

Uh, prefiero no pagar el precio por la coherencia multiplataforma : / Es basura, no me importa qué basura salga (sin embargo, una afirmación de depuración sería muy útil).

Actualmente, todas las funciones de redondeo / truncado en Rust son muy lentas (llamadas a funciones con implementaciones minuciosamente precisas), por lo que as es mi último recurso para el redondeo flotante rápido.

Si va a hacer as algo más que cvttss2si , agregue también una alternativa estable que sea solo eso.

@pornel, esto no es solo UB de tipo teórico donde las cosas están bien si ignoras que es ub, tiene implicaciones en el mundo real. Extraje el # 41799 de un ejemplo de código del mundo real.

@ est31 Estoy de acuerdo en que dejarlo como UB está mal, pero he visto freeze propuesto como una solución para UB. AFAIK, eso lo convierte en un valor determinista definido, simplemente no puedes decir cuál. Ese comportamiento está bien para mí.

Así que estaría bien si, por ejemplo, u128::MAX as f32 produjera determinísticamente 17.5 en x86, y 999.0 en x86-64 y -555 en ARM.

freeze no produciría un valor definido, determinista y no especificado. Su resultado sigue siendo "cualquier patrón de bits que le guste al compilador", y es coherente sólo en los usos de la misma operación. Esto puede eludir los ejemplos de producción de UB que la gente ha recopilado anteriormente, pero no daría esto:

u128 :: MAX como f32 produjo de manera determinista 17.5 en x86, 999.0 en x86-64 y -555 en ARM.

Por ejemplo, si LLVM nota que u128::MAX as f32 desborda y lo reemplaza con freeze poison , una reducción válida de fn foo() -> f32 { u128::MAX as f32 } en x86_64 podría ser la siguiente:

foo:
  ret

(es decir, devuelva lo que se almacenó por última vez en el registro de devolución)

Veo. Eso sigue siendo aceptable para mis usos (para los casos en los que espero valores fuera del rango, hago la sujeción de antemano. Donde espero valores en el rango, pero no lo son, entonces no obtendré un resultado correcto sin importar qué) .

No tengo ningún problema con los lanzamientos flotantes fuera de rango que devuelven valores arbitrarios siempre que los valores estén congelados para que no puedan causar más comportamientos indefinidos.

¿Hay algo como freeze disponible en LLVM? Pensé que era una construcción puramente teórica.

@nikomatsakis Nunca lo había visto usado así (a diferencia de poison ): es una renovación planificada de poison / undef.

freeze no existe en absoluto en LLVM hoy. Solo se ha propuesto ( este documento PLDI es una versión independiente, pero también se ha discutido mucho en la lista de correo). La propuesta parece tener una aceptación considerable, pero, por supuesto, eso no es garantía de que sea adoptada, y mucho menos adoptada de manera oportuna. (La eliminación de los tipos de punteros de los tipos de punteros se ha aceptado durante años y todavía no se ha hecho).

¿Queremos abrir una RFC para lograr un debate más amplio sobre los cambios que se proponen aquí? En mi opinión, cualquier cosa que pueda afectar el rendimiento de as va a ser polémico, pero será doblemente polémico si no le damos a la gente la oportunidad de hacer oír su voz.

Soy un desarrollador de Julia y he estado siguiendo este problema durante un tiempo, ya que compartimos el mismo backend LLVM y, por lo tanto, tenemos problemas similares. En caso de que sea de interés, esto es lo que nos hemos decidido (con tiempos aproximados para una sola función en mi máquina):

  • unsafe_trunc(Int64, x) asigna directamente al LLVM intrínseco fptosi (1.5 ns) correspondiente
  • trunc(Int64, x) lanza una excepción para valores fuera de rango (3 ns)
  • convert(Int64, x) arroja una excepción para valores fuera de rango o no enteros (6 ns)

Además, he preguntado en la lista de correo sobre cómo definir un poco más el comportamiento indefinido, pero no recibí una respuesta muy prometedora.

@bstrie Estoy bien con un RFC, ¡pero creo que definitivamente sería útil tener datos! Sin embargo , el comentario de

He jugado con la semántica de JS (el módulo @jorendorff mencionado) y la semántica de Java que parece ser la columna de "saturación". En caso de que esos enlaces caduquen, es JS y Java .

También preparé una implementación rápida de saturación en Rust que creo (?) Es correcta. Y también obtuve algunos números de referencia . Curiosamente, estoy viendo que la implementación de saturación es 2-3 veces más lenta que la intrínseca, que es diferente de lo que @simonbyrne encontró con solo 2 veces más lenta.

No estoy del todo seguro de cómo implementar la semántica "mod" en Rust ...

Para mí, sin embargo, parece claro que necesitaremos una gran cantidad de métodos f32::as_u32_unchecked() y demás para aquellos que necesitan el rendimiento.

parece claro que necesitaremos una gran cantidad de métodos f32::as_u32_unchecked() y demás para aquellos que necesitan el rendimiento.

Eso es un fastidio, ¿o te refieres a una variante segura pero definida por la implementación?

¿No hay opción para una implementación predeterminada rápida definida?

@eddyb Estaba pensando que tendríamos unsafe fn as_u32_unchecked(self) -> u32 en f32 y eso es un análogo directo de lo que as es hoy.

Ciertamente, no voy a afirmar que la implementación de Rust que escribí sea óptima, pero tenía la impresión de que al leer este hilo, el determinismo y la seguridad eran más importantes que la velocidad en este contexto la mayor parte del tiempo. La trampilla de escape unsafe es para aquellos que están al otro lado de la cerca.

¿Entonces no hay una variante barata dependiente de la plataforma? Quiero algo que sea rápido, que dé un valor no especificado cuando esté fuera de los límites y sea ​​seguro. No quiero UB para algunas entradas y creo que eso es demasiado peligroso para el uso común, si podemos hacerlo mejor.

Hasta donde yo sé, en la mayoría de las plataformas, si no en todas, la forma canónica de implementar esta conversión hace algo en las entradas fuera de rango que no es UB. Pero LLVM no parece tener ninguna forma de elegir esa opción (cualquiera que sea) sobre UB. Si pudiéramos convencer a los desarrolladores de LLVM para que introduzcan un valor intrínseco que produzca un resultado "no especificado pero no undef / poison " en entradas fuera de rango, podríamos usar eso.

Pero estimo que alguien en este hilo tendría que escribir un RFC convincente (en la lista llvm-dev), obtener la aceptación e implementarlo (en los backends que nos interesan y con una implementación alternativa para otros objetivos). Probablemente sea más fácil que convencer a llvm-dev para que haga que las conversiones existentes no sean UB (porque evita preguntas como "¿hará que los programas en C y C ++ sean más lentos"), pero aún así no es muy fácil.

En caso de que elija entre estos:

Saturación (los valores fuera de rango se convierten en IntType :: max_value () / min_value ())
Módulo (los valores fuera de rango se tratan como si primero se convirtieran a un bigint y luego se truncaran)

En mi opinión, solo la saturación tendría sentido aquí, porque la precisión absoluta del punto flotante cae rápidamente a medida que los valores aumentan, por lo que en algún momento el módulo sería algo inútil como todos los ceros.

Marqué esto como E-needs-mentor y lo etiqueté con WG-compiler-middle ya que parece que el período implícito podría ser un buen momento para investigar esto más. Sin embargo , mis sobre el plan de registro son bastante escasas , por lo que sería genial si alguien de @ rust-lang / compiler quisiera ayudar a elaborarlas un poco más.

@nikomatsakis

IIRC LLVM planea implementar eventualmente freeze , lo que debería permitirnos lidiar con el UB haciendo un freeze .

Mis resultados hasta ahora: https://gist.github.com/s3bk/4bdfbe2acca30fcf587006ebb4811744

Las variantes de _array ejecutan un ciclo de 1024 valores.
_cast: x as i32
_clip: x.min (MAX) .max (MIN) como i32
_panic: entra en pánico si x está fuera de los límites
_zero: establece el resultado en cero si está fuera de límites

test bench_array_cast       ... bench:       1,840 ns/iter (+/- 37)
test bench_array_cast_clip  ... bench:       2,657 ns/iter (+/- 13)
test bench_array_cast_panic ... bench:       2,397 ns/iter (+/- 20)
test bench_array_cast_zero  ... bench:       2,671 ns/iter (+/- 19)
test bench_cast             ... bench:           2 ns/iter (+/- 0)
test bench_cast_clip        ... bench:           2 ns/iter (+/- 0)
test bench_cast_panic       ... bench:           2 ns/iter (+/- 0)
test bench_cast_zero        ... bench:           2 ns/iter (+/- 0)

Quizás no necesite redondear los resultados a números enteros para operaciones individuales. Claramente debe haber alguna diferencia detrás de estos 2 ns / iter. ¿O es realmente así, _exactamente_ 2 ns para las 4 variantes?

@ sp-1234 Me pregunto si está parcialmente optimizado.

@ sp-1234 Es demasiado rápido de medir. Los puntos de referencia que no son de matriz son básicamente inútiles.
Si fuerza las funciones de valor único a ser funciones a través de #[inline(never)] , obtiene 2ns vs 3ns.

@ arielb1
Tengo algunas reservas con respecto a freeze . Si lo entiendo correctamente, un undef congelado aún puede contener cualquier valor arbitrario, simplemente no cambiará entre usos. En la práctica, el compilador probablemente reutilizará un registro o una ranura de pila.

Sin embargo, esto significa que ahora podemos leer la memoria no inicializada del código seguro. Esto podría provocar la filtración de datos secretos, algo así como Heartbleed. Es discutible si esto realmente se considera UB desde el punto de vista de Rust, pero claramente parece indeseable.

Ejecuté el punto de referencia de

Desafortunadamente, enviar spam black_box no parece ayudar. Veo que el asm está haciendo un trabajo útil, pero ejecutar el punto de referencia todavía da 0ns para los puntos de referencia escalares (excepto cast_zero , que muestra 1ns). Veo que @alexcrichton realizó la comparación 100 veces en sus puntos de referencia, así que adopté el mismo truco. Ahora veo estos números ( código fuente ):

test bench_cast             ... bench:          53 ns/iter (+/- 0)
test bench_cast_clip        ... bench:         164 ns/iter (+/- 1)
test bench_cast_panic       ... bench:         172 ns/iter (+/- 2)
test bench_cast_zero        ... bench:         100 ns/iter (+/- 0)

Los puntos de referencia de la matriz varían demasiado para que pueda confiar en ellos. Bueno, la verdad es que soy escéptico de la infraestructura de evaluación comparativa test todos modos, especialmente después de ver los números anteriores en comparación con los 0ns planos que obtuve anteriormente. Además, incluso solo 100 iteraciones de black_box(x); (como línea de base) toman 34ns, lo que hace que sea aún más difícil interpretar esos números de manera confiable.

Dos puntos a destacar:

  • A pesar de no manejar NaN específicamente (devuelve -inf en lugar de 0?), La implementación de cast_clip parece ser más lenta que el reparto de saturación de @alexcrichton (tenga en cuenta que su ejecución y la mía tienen aproximadamente el mismo tiempo para as yesos, 53-54ns).
  • A diferencia de los resultados de la matriz de @ s3bk , veo que cast_panic es más lento que las otras conversiones verificadas. También veo una desaceleración aún mayor en los puntos de referencia de las matrices. ¿Quizás estas cosas dependen en gran medida de los detalles de la microarquitectura y / o del estado de ánimo del optimizador?

Para el registro, he medido con rustc 1.21.0-nightly (d692a91fa 2017-08-04), -C opt-level=3 , en un i7-6700K con carga ligera.


En conclusión, llego a la conclusión de que no tenemos datos confiables hasta ahora y que obtener datos más confiables parece difícil. Además, dudo mucho que cualquier aplicación real dedique ni el 1% de su tiempo de reloj de pared a esta operación. Por lo tanto, sugeriría avanzar implementando la saturación de as fundidos en rustc , detrás de una bandera -Z , y luego ejecutando algunos puntos de referencia no artificiales con y sin esta bandera para determinar el impacto en realistas aplicaciones.

Editar: también recomendaría ejecutar dichos puntos de referencia en una variedad de arquitecturas (por ejemplo, incluyendo ARM) y microarquitecturas, si es posible.

Admito que no estoy tan familiarizado con el óxido, pero creo que esta línea es sutilmente incorrecta: std::i32::MAX (2 ^ 31-1) no es exactamente representable como Float32, por lo que std::i32::MAX as f32 será redondeado al valor representable más cercano (2 ^ 31). Si este valor se usa como argumento x , el resultado es técnicamente indefinido. Reemplazar con una desigualdad estricta debería solucionar este caso.

Sí, tuvimos exactamente ese problema en Servo antes. La solución final fue lanzar a f64 y luego sujetar.

Hay otras soluciones, pero son bastante complicadas y el óxido no expone buenas API para lidiar bien con esto.

usar 0x7FFF_FF80i32 como límite superior y -0x8000_0000i32 debería resolver esto sin pasar a f64.
editar: use el valor correcto.

Creo que te refieres a 0x7fff_ff80 , pero el simple hecho de usar una desigualdad estricta probablemente aclarará la intención del código.

como en x < 0x8000_0000u32 as f32 ? Probablemente sería una buena idea.

Pienso en todas las opciones deterministas sugeridas, la sujeción es generalmente la más útil porque creo que se hace a menudo de todos modos. Si realmente se documentara que el tipo de conversión es saturante, la sujeción manual sería innecesaria.

Solo estoy un poco preocupado por la implementación sugerida porque no se traduce correctamente a las instrucciones de la máquina y depende en gran medida de la ramificación. La ramificación hace que el rendimiento dependa de patrones de datos específicos. En los casos de prueba dados anteriormente, todo parece (comparativamente) rápido porque siempre se toma la misma rama y el procesador tiene buenos datos de predicción de rama de muchas iteraciones de bucle anteriores. El mundo real probablemente no se verá así. Además, la ramificación perjudica la capacidad del compilador para vectorizar el código. No estoy de acuerdo con la opinión de @rkruppe de que la operación no debería probarse también en combinación con la vectorización. La vectorización es importante en el código de alto rendimiento y ser capaz de vectorizar moldes simples en arquitecturas comunes debería ser un requisito crucial.

Por las razones expuestas anteriormente, jugué con una versión alternativa sin ramas y orientada al flujo de datos del elenco de corrección de u16 , i16 e i32, ya que todos tienen que cubrir casos ligeramente diferentes que dan como resultado un rendimiento variable.

Los resultados:

test i16_bench_array_cast       ... bench:          99 ns/iter (+/- 2)
test i16_bench_array_cast_clip  ... bench:         197 ns/iter (+/- 3)
test i16_bench_array_cast_clip2 ... bench:         113 ns/iter (+/- 3)
test i16_bench_cast             ... bench:          76 ns/iter (+/- 1)
test i16_bench_cast_clip        ... bench:         218 ns/iter (+/- 25)
test i16_bench_cast_clip2       ... bench:         148 ns/iter (+/- 4)
test i16_bench_rng_cast         ... bench:       1,181 ns/iter (+/- 17)
test i16_bench_rng_cast_clip    ... bench:       1,952 ns/iter (+/- 27)
test i16_bench_rng_cast_clip2   ... bench:       1,287 ns/iter (+/- 19)

test i32_bench_array_cast       ... bench:         114 ns/iter (+/- 1)
test i32_bench_array_cast_clip  ... bench:         200 ns/iter (+/- 3)
test i32_bench_array_cast_clip2 ... bench:         128 ns/iter (+/- 3)
test i32_bench_cast             ... bench:          74 ns/iter (+/- 1)
test i32_bench_cast_clip        ... bench:         168 ns/iter (+/- 3)
test i32_bench_cast_clip2       ... bench:         189 ns/iter (+/- 3)
test i32_bench_rng_cast         ... bench:       1,184 ns/iter (+/- 13)
test i32_bench_rng_cast_clip    ... bench:       2,398 ns/iter (+/- 41)
test i32_bench_rng_cast_clip2   ... bench:       1,349 ns/iter (+/- 19)

test u16_bench_array_cast       ... bench:          99 ns/iter (+/- 1)
test u16_bench_array_cast_clip  ... bench:         136 ns/iter (+/- 3)
test u16_bench_array_cast_clip2 ... bench:         105 ns/iter (+/- 3)
test u16_bench_cast             ... bench:          76 ns/iter (+/- 2)
test u16_bench_cast_clip        ... bench:         184 ns/iter (+/- 7)
test u16_bench_cast_clip2       ... bench:         110 ns/iter (+/- 0)
test u16_bench_rng_cast         ... bench:       1,178 ns/iter (+/- 22)
test u16_bench_rng_cast_clip    ... bench:       1,336 ns/iter (+/- 26)
test u16_bench_rng_cast_clip2   ... bench:       1,207 ns/iter (+/- 21)

La prueba se ejecutó en una CPU Intel Haswell i5-4570 y Rust 1.22.0 todas las noches.
clip2 es la nueva implementación sin ramas. Coincide con clip en los 2 ^ 32 posibles valores de entrada de f32.

Para los puntos rng referencia

Comparación de montaje en x86: https://godbolt.org/g/AhdF71
La versión sin ramas se asigna muy bien a las instrucciones minss / maxss.

Desafortunadamente, no pude hacer que godbolt generara un ensamblaje ARM desde Rust, pero aquí hay una comparación ARM de los métodos con Clang: https://godbolt.org/g/s7ronw
Sin poder probar el código y saber mucho de ARM: el tamaño del código también parece más pequeño y LLVM genera principalmente vmax / vmin, lo que parece prometedor. ¿Quizás se podría enseñar a LLVM eventualmente a plegar la mayor parte del código en una sola instrucción?

@ActuallyaDeviloper ¡ El rustc que los condicionales anidados de otras soluciones (para el registro, supongo que queremos generar IR en línea en lugar de llamar a una función de elemento lang). Muchas gracias por escribir esto.

Tengo una pregunta sobre u16_cast_clip2 : ¡¿no parece manejar NaN ?! Hay un comentario que habla de NaN, pero creo que la función pasará NaN sin modificar e intentará convertirlo en f32 (e incluso si no lo hiciera, produciría uno de los valores de límite en lugar de 0 ).

PD: Para ser claro, no estaba tratando de dar a entender que no es importante si el elenco se puede vectorizar. Claramente, es importante si el código circundante es vectorizable. Pero el rendimiento escalar también es importante, ya que la vectorización a menudo no es aplicable y los puntos de referencia que estaba comentando no hacían ninguna declaración sobre el rendimiento escalar. Por interés, ¿ha verificado el conjunto de los puntos de referencia *array* para ver si todavía están vectorizados con su implementación?

@rkruppe Tienes razón, accidentalmente cambié los lados del if y me olvidé de eso. f32 as u16 sucedió para hacer lo correcto al truncar el 0x8000 superior, por lo que las pruebas tampoco lo detectaron. Solucioné el problema ahora intercambiando las ramas nuevamente y probando todos los métodos con if (y.is_nan()) { panic!("NaN"); } esta vez.

Actualicé mi publicación anterior. El código x86 no cambió significativamente en absoluto, pero desafortunadamente el cambio evita que LLVM genere vmax en el caso u16 ARM por alguna razón. Supongo que esto tiene que ver con algunos detalles sobre el manejo de NaN de esa instrucción ARM o tal vez es una limitación de LLVM.

Por qué funciona, observe que el valor de límite inferior es en realidad 0 para valores sin signo. De modo que NaN y el límite inferior se pueden capturar al mismo tiempo.

Las versiones de matriz están vectorizadas.
Godbolt: https://godbolt.org/g/HnmsSV

Re: el ARM asm , creo que la razón por la que vmax ya no se usa es que devuelve NaN si alguno de los operandos es NaN . Sin embargo, el código todavía no tiene ramificaciones, solo usa movimientos predicados ( vmovgt , refiriéndose al resultado del anterior vcmp con 0).

Por qué funciona, observe que el valor de límite inferior es en realidad 0 para valores sin signo. De modo que NaN y el límite inferior se pueden capturar al mismo tiempo.

Ohhh, cierto. Agradable.

Sugeriría avanzar implementando la saturación como moldes en rustc, detrás de una bandera -Z

He implementado esto y presentaré un PR una vez que también haya solucionado el número 41799 y tenga muchas más pruebas.

45134 ha señalado una ruta de código que me perdí (generación de expresiones constantes LLVM; esto es independiente de la propia evaluación constante de rustc). Lanzaré una solución para eso en el mismo PR, pero tomará un poco más de tiempo.

@rkruppe Debes coordinar con @ oli-obk para que miri termine con los mismos cambios.

La solicitud de extracción ha aumentado: # 45205

45205 se ha fusionado, por lo que ahora cualquiera puede (bueno, comenzando con la siguiente noche) medir el impacto de la saturación en -Z saturating-float-casts través de RUSTFLAGS . [1] Estas mediciones serían muy valiosas para decidir cómo proceder con este problema.

[1] Estrictamente hablando, esto no afectará las porciones no genéricas, no #[inline] de la biblioteca estándar, por lo que para ser 100% exacto, querrá construir localmente std con Xargo. Sin embargo, no espero que haya mucho código afectado por esto (las diversas implicaciones de rasgos de conversión son #[inline] , por ejemplo).

@rkruppe Sugiero iniciar una página interna / usuarios para recopilar datos, en la misma línea que https://internals.rust-lang.org/t/help-us-benchmark-incremental-compilation/6153/ (entonces también podemos vincular a las personas a eso, en lugar de algunos comentarios aleatorios en nuestro rastreador de problemas)

@rkruppe deberías crear un problema de seguimiento. Esta discusión ya está dividida en dos temas. ¡Eso no es bueno!

@Gankro Sí, estoy de acuerdo, pero pueden pasar unos días antes de que encuentre el tiempo para escribir esa publicación correctamente, así que pensé que, mientras tanto, solicitaría comentarios de las personas suscritas a este tema.

@ est31 Hmm. Aunque la bandera -Z cubre ambas direcciones de lanzamiento (lo que puede haber sido un error, en retrospectiva), parece poco probable que activemos el interruptor en ambos al mismo tiempo, y hay poca superposición entre los dos en términos de lo que debe ser discutido (por ejemplo, este problema depende del desempeño de la saturación, mientras que en el # 41799 se acuerda cuál es la solución correcta).
Es un poco tonto que los puntos de referencia principalmente dirigidos a este problema sería también medir el impacto de la solución a # 41799, pero que puede a lo sumo plomo a overreporting de regresiones de rendimiento, así que soy una especie de acuerdo con eso. (Pero si alguien está motivado a dividir la bandera -Z en dos, adelante).

He considerado un problema de seguimiento para la tarea de eliminar la bandera una vez que ha dejado de ser útil, pero no veo la necesidad de fusionar las discusiones que ocurren aquí y en el # 41799.

He redactado una publicación interna: https://gist.github.com/Gankro/feab9fb0c42881984caf93c7ad494ebd

Siéntete libre de copiarlo o simplemente dame notas para que pueda publicarlo. (tenga en cuenta que estoy un poco confundido sobre el comportamiento de const fn )

Un dato adicional es que el costo de las conversiones float-> int es específico de la implementación actual, en lugar de ser fundamental. En x86, cvtss2si cvttss2si devuelve 0x80000000 en los casos demasiado bajo, demasiado alto y nan, por lo que se podría implementar -Zsaturating-float-casts con un cvtss2si cvttss2si seguido de un código especial en el caso 0x80000000, por lo que podría ser una sola rama de comparación y predicción en el caso común. En ARM, vcvt.s32.f32 tiene la semántica -Zsaturating-float-casts . Actualmente, LLVM no optimiza las comprobaciones adicionales en ninguno de los casos.

@Gankro

¡Increíble, muchas gracias! Dejé algunas notas sobre la esencia. Después de leer esto, me gustaría intentar separar los lanzamientos de u128-> f32 de la bandera -Z. Solo por el bien de deshacerse de la advertencia que distrae acerca de la bandera que cubre dos características ortogonales.

(Presenté # 45900 para reenfocar la bandera -Z de modo que solo cubra el problema de flotación-> int)

Sería bueno si pudiéramos obtener implementaciones específicas de la plataforma a la @sunfishcode (al menos para x86) antes de solicitar evaluaciones comparativas masivas. No debería ser muy difícil.

El problema es que LLVM no proporciona actualmente una forma de hacer esto, hasta donde yo sé, excepto tal vez con un conjunto en línea que no recomendaría necesariamente para una versión.

He actualizado el borrador para reflejar la discusión (básicamente eliminando cualquier mención en línea de u128 -> f32 a una sección adicional al final).

@sunfishcode ¿Estás seguro? ¿No es el llvm.x86.sse.cvttss2si intrínseco lo que estás buscando?

Aquí hay un enlace de juegos que lo usa:

https://play.rust-lang.org/?gist=33cf9e0871df2eb2475b845af4f1b574&version=nightly

En el modo de lanzamiento, float_to_int_with_intrinsic y float_to_int_with_as compilan en una sola instrucción. (En el modo de depuración, float_to_int_with_intrinsic desperdicia algunas instrucciones poniendo cero en el máximo, pero no es tan malo).

Incluso parece hacer un plegado constante correctamente. Por ejemplo,

float_to_int_with_intrinsic(42.0)

se convierte en

movl    $42, %eax

Pero un valor fuera de rango,

float_to_int_with_intrinsic(42.0e33)

no se dobla:

cvttss2si   .LCPI2_0(%rip), %eax

(Idealmente, se doblaría a 0x80000000 constante, pero eso no es gran cosa. Lo importante es que no produce undef).

Oh, genial. ¡Parece que funcionaría!

Es genial saber que, después de todo, tenemos una forma de construir sobre cvttss2si . Sin embargo, no estoy de acuerdo con que sea claramente mejor cambiar la implementación para usarla antes de solicitar puntos de referencia:

La mayoría de la gente comparará en x86, por lo que si usamos x86 en un caso especial, obtendremos muchos menos datos sobre la implementación general, que aún se utilizará en la mayoría de los otros objetivos. Es cierto que ya es difícil inferir algo sobre otras arquitecturas, pero una implementación completamente diferente lo hace totalmente imposible.

En segundo lugar, si recopilamos puntos de referencia ahora, con la solución "simple", y descubrimos que no hay regresiones de rendimiento en el código real (y eso es lo que espero), entonces ni siquiera necesitamos tomarnos la molestia de intentar optimizar aún más esta ruta de código.

Finalmente, ni siquiera estoy seguro de que construir sobre cvttss2si sea ​​más rápido de lo que tenemos ahora (aunque en ARM, es claramente mejor usar la instrucción apropiada):

  • Necesita una comparación para notar que la conversión devuelve 0x80000000, y si ese es el caso, aún necesita otra comparación (del valor de entrada) para saber si debe devolver int :: MIN o int :: MAX. Y si es un tipo entero con signo, no veo cómo evitar una tercera comparación para distinguir NaN. Entonces, en el peor de los casos:

    • no ahorras en el número de comparaciones / selecciones

    • está intercambiando una comparación flotante por una comparación int, lo que podría ser bueno para los núcleos OoO (si tiene un cuello de botella en las UF que pueden hacer comparaciones, lo que parece un si relativamente grande), pero esa comparación también depende de la flotación -> int comparación, mientras que las comparaciones en la implementación actual son todas independientes, por lo que no es obvio que esto sea una victoria.

  • La vectorización probablemente se vuelve más difícil o imposible. No espero que el vectorizador de bucle maneje esto intrínseco en absoluto.
  • También vale la pena señalar que (AFAIK) esta estrategia solo se aplica a algunos tipos de enteros. f32 -> u8, por ejemplo, necesitará arreglos adicionales del resultado, lo que hace que esta estrategia claramente no sea rentable. No estoy muy seguro de qué tipos se ven afectados por esto (por ejemplo, no sé si hay una instrucción para f32 -> u32), pero una aplicación que usa solo esos tipos no se beneficiará en absoluto.
  • Podría hacer una solución de ramificación con solo una comparación en el camino feliz (en lugar de dos o tres comparaciones y, por lo tanto, ramificaciones, como lo hacían las soluciones anteriores). Sin embargo, como @ActuallyaDeviloper argumentó anteriormente, la ramificación puede no ser deseable: el rendimiento ahora se vuelve aún más dependiente de la carga de trabajo y dependiente de la predicción de la ramificación.

¿Es seguro asumir que vamos a necesitar una gran cantidad de unsafe fn as_u32_unchecked(self) -> u32 y amigos, independientemente de lo que muestre la evaluación comparativa? ¿Qué otras posibilidades de recurrir alguien tendría si no terminan observando una desaceleración?

@bstrie Creo que tendría más sentido, en un caso como ese, hacer algo como extender la sintaxis a as <type> [unchecked] y requerir que unchecked solo esté presente en unsafe contextos.

A mi modo de ver, un bosque de _unchecked funciona como variantes de as casting sería una verruga, tanto en lo que respecta a la intuición como en lo que respecta a generar documentación limpia y utilizable.

@ssokolow Agregar sintaxis siempre debe ser el último recurso, especialmente si todo esto se puede foo.as_unchecked::<u32>() genérico sería preferible a los cambios sintácticos (y el interminable bicished concomitante), especialmente porque deberíamos reducir, no aumentar, la cantidad de cosas que unsafe desbloquea.

Punto. El turbofish se me olvidó al considerar las opciones y, en retrospectiva, tampoco estoy disparando a todos los cilindros esta noche, por lo que debería haber sido más cauteloso al comentar sobre las decisiones de diseño.

Dicho esto, se siente mal incluir el tipo de destino en el nombre de la función ... poco elegante y una carga potencial para la evolución futura del lenguaje. El pez turbio se siente como una mejor opción.

Un método genérico podría estar respaldado por un nuevo conjunto de UncheckedFrom / UncheckedInto rasgos con unsafe fn métodos, uniendo los From / Into y TryFrom / TryInto colección.

@bstrie Una solución alternativa para las personas cuyo código se volvió más lento podría ser utilizar un intrínseco (por ejemplo, a través de stdsimd) para acceder a la instrucción de hardware subyacente. Argumenté anteriormente que esto tiene desventajas para el optimizador: es probable que la vectorización automática sufra, y LLVM no puede explotarlo devolviendo undef en entradas fuera de rango, pero ofrece una forma de hacer el lanzamiento sin cualquier trabajo adicional en tiempo de ejecución. No puedo decidir si esto es lo suficientemente bueno, pero parece al menos plausible que pueda serlo.

Algunas notas sobre conversiones en el conjunto de instrucciones x86:

SSE2 es relativamente limitado en las operaciones de conversión que le ofrece. Tienes:

  • Familia CVTTSS2SI con registro de 32 bits: convierte un solo float en i32
  • Familia CVTTSS2SI con registro de 64 bits: convierte un flotador único en i64 (solo x86-64)
  • Familia CVTTPS2PI: convierte dos flotantes en dos i32s

Cada uno de ellos tiene variantes para f32 y f64 (así como variantes que redondean en lugar de truncar, pero eso es inútil aquí).

Pero no hay nada para enteros sin firmar, nada para tamaños menores a 32, y si está en x86 de 32 bits, nada para 64 bits. Las extensiones de conjuntos de instrucciones posteriores agregan más funcionalidad, pero parece que casi nadie compila para ellas.

Como resultado, el comportamiento existente ('inseguro'):

  • Para convertir a u32, los compiladores convierten a i64 y truncan el entero resultante. (Esto produce un comportamiento extraño para valores fuera de rango, pero eso es UB, así que a quién le importa).
  • Para convertir a cualquier cosa de 16 u 8 bits, los compiladores convierten a i64 o i32 y truncan el entero resultante.
  • Para convertir a u64, los compiladores generan una gran cantidad de instrucciones. Para f32 a u64 GCC y LLVM genere un equivalente de:
fn f32_to_u64(f: f32) -> u64 {
    const CUTOFF: f32 = 0x8000000000000000 as f32; // 2^63 exactly
    if !(f >= CUTOFF) { // less, or NaN
        // just use the signed conversion
        f as i64 as u64
    } else {
        0x8000000000000000u64 + ((f - CUTOFF) as i64 as u64)
    }
}

Dato curioso no relacionado: la generación de código "convertir-que-truncar" es lo que causa la falla de " universos paralelos " en Super Mario 64. El código de detección de colisión primero instrucción MIPS para convertir coordenadas f32 a i32, luego trunca a i16; por lo tanto, las coordenadas que se ajustan a i16 pero no a i32 'envuelven', por ejemplo, ir a la coordenada 65536.0 le brindan detección de colisión por 0.0.

De todos modos, conclusiones:

  • "Probar 0x80000000 y tener un controlador especial" solo funciona para conversiones a i32 e i64.
  • Sin embargo, para las conversiones a u32, u / i16 y u / i8, "probar si la salida truncada / extendida con signo difiere de la original" es un equivalente. (Esto recogería ambos enteros que estaban dentro del rango para la conversión original pero fuera del rango para el tipo final, y 0x8000000000000000, el indicador de que el flotante era NaN o fuera del rango para la conversión original).
  • Pero el costo de una sucursal y un montón de código adicional para ese caso probablemente sea excesivo. Puede estar bien si se pueden evitar las ramas.
  • @ActuallyaDeviloper 's minss / maxss enfoque no es tan malo! La forma mínima,
minss %xmm2, %xmm1
maxss %xmm3, %xmm1
cvttss2si %rax, %xmm1

son solo tres instrucciones (que tienen un tamaño de código decente y rendimiento / latencia) y no tienen ramas.

Sin embargo:

  • La versión pure-Rust necesita una prueba adicional para NaN. Para las conversiones a 32 bits o menos, eso se puede evitar usando intrínsecos, usando cvttss2si de 64 bits y truncando el resultado. Si la entrada no fue NaN, el mínimo / máximo asegura que el número entero no cambie por truncamiento. Si la entrada fue NaN, el número entero es 0x8000000000000000 que se trunca a 0.
  • No incluí el costo de cargar 2147483647.0 y -2148473648.0 en los registros, generalmente un movimiento de la memoria cada uno.
  • Para f32, 2147483647.0 no se puede representar exactamente, por lo que en realidad no funciona: necesita otra verificación. Eso empeora las cosas. Lo mismo ocurre con f64 to u / i64, pero f64 to u / i32 no tiene este problema.

Sugiero un compromiso entre los dos enfoques:

  • Para f32 / f64 a u / i16 y u / i8, y f64 a u / i32, vaya con min / max + truncamiento, como arriba, por ejemplo:
    let f = if f > 32767.0 { 32767.0 } else { f };
    let f = if f < -32768.0 { -32768.0 } else { f };
    cvttss2si(f) as i16

(Para u / i16 y u / i8, la conversión original puede ser a i32; para f64 a u / i32, debe ser a i64).

  • Para f32 / 64 a u32,
    let r = cvttss2si64(f) as u32;
    if f >= 4294967296.0 { 4294967295 } else { r }

son solo unas pocas instrucciones y sin ramas:

    cvttss2si   %xmm0, %rcx
    ucomiss .LCPI0_0(%rip), %xmm0
    movl    $-1, %eax
    cmovbl  %ecx, %eax
  • Para f32 / 64 a i64, tal vez
    let r = cvttss2si64(f);
    if f >= 9223372036854775808. {
        9223372036854775807 
    } else if f != f {
        0
    } else {
        r
    }

Esto produce una secuencia más larga (aún sin ramificaciones):

    cvttss2si   %xmm0, %rax
    xorl    %ecx, %ecx
    ucomiss %xmm0, %xmm0
    cmovnpq %rax, %rcx
    ucomiss .LCPI0_0(%rip), %xmm0
    movabsq $9223372036854775807, %rax
    cmovbq  %rcx, %rax

… Pero al menos guardamos una comparación en comparación con el enfoque ingenuo, como si f demasiado pequeño, 0x8000000000000000 ya es la respuesta correcta (es decir, i64 :: MIN).

  • Para f32 a i32, no estoy seguro de si sería preferible hacer lo mismo que antes, o simplemente convertir a f64 primero y luego hacer lo más corto de min / max.

  • u64 es un desastre en el que no tengo ganas de pensar. :pags

Ha finalizado la convocatoria de evaluaciones comparativas: https://internals.rust-lang.org/t/help-us-benchmark-saturating-float-casts/6231

En https://internals.rust-lang.org/t/help-us-benchmark-saturating-float-casts/6231/14 alguien informó una desaceleración significativa y medible en la codificación JPEG con la caja de imágenes. He minimizado el programa para que sea autónomo y se centre principalmente en las partes relacionadas con la desaceleración: https://gist.github.com/rkruppe/4e7972a209f74654ebd872eb4bc57722 (este programa muestra ~ 15% de desaceleración para mí con saturación yesos).

Tenga en cuenta que los moldes son f32-> u8 ( rgb_to_ycbcr ) y f32-> i32 ( encode_rgb , bucle "Cuantización") en proporciones iguales. También parece que todas las entradas están dentro del rango, es decir, la saturación nunca se activa, pero en el caso de f32-> u8 esto solo se puede verificar calculando el mínimo y máximo de un polinomio y teniendo en cuenta el error de redondeo, que es mucho pedir. Las conversiones f32-> i32 están más obviamente dentro del rango para i32, pero solo porque los elementos de self.tables son distintos de cero, lo que (¿aparentemente?) No es tan fácil de mostrar para el optimizador, especialmente en el programa original. tl; dr: Los controles de saturación están ahí para quedarse, la única esperanza es hacerlos más rápidos.

También he analizado un poco el LLVM IR: parece que literalmente la única diferencia son las comparaciones y selecciones de los modelos saturados. Un vistazo rápido indica que el ASM tiene las instrucciones correspondientes y, por supuesto, muchos más valores en vivo (que conducen a más derrames).

@comex ¿Crees que los moldes f32-> u8 y f32-> i32 se pueden realizar considerablemente más rápido con CVTTSS2SI?

Actualización menor, a partir de rustc 1.28.0-nightly (952f344cd 2018-05-18) , la marca -Zsaturating-float-casts todavía hace que el código en https://github.com/rust-lang/rust/issues/10184#issuecomment -345479698 sea ~ 20 % más lento en x86_64. Lo que significa que LLVM 6 no ha cambiado nada.

| Banderas Tiempo |
| ------- | -------: |
| -Nivel-copia = 3 -Cpu-objetivo-cpu = nativo | 325.699 ns / iter (+/- 7.607) |
| -Nivel-copto = 3 -Cpu-objetivo-cpu = nativo -Zsaturating-float-cast | 386,962 ns / iter (+/- 11.601)
(19% más lento) |
| -Nivel-copto = 3 | 331.521 ns / iter (+/- 14.096) |
| -Nivel-copto = 3 -Zsaturantes-flotantes 413,572 ns / iter (+/- 19,183)
(25% más lento) |

@kennytm ¿

@insanitybit ¿Parece que todavía está abierto ...?

image

Bueno, no tengo ni idea de lo que estaba mirando. ¡Gracias!

@rkruppe ¿no nos aseguramos de que las
(cambiando documentos)?

El 20 de julio de 2018 a las 4:31 a. M., "Colin" [email protected] escribió:

Bueno, no tengo ni idea de lo que estaba mirando.

-
Estás recibiendo esto porque estás suscrito a este hilo.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/rust-lang/rust/issues/10184#issuecomment-406462053 ,
o mudo
la amenaza
https://github.com/notifications/unsubscribe-auth/AApc0v3rJHhZMD7Kv7RC8xkGOiIhkGB1ks5uITMHgaJpZM4BJ45C
.

@nagisa ¿ Quizás estás pensando en f32::from_bits(v: u32) -> f32 (y de manera similar f64 )? Solía ​​hacer algo de normalización de NaN, pero ahora es solo transmute .

Este problema se trata de conversiones de as que intentan aproximarse al valor numérico.

Ah, sí, eso era flotar para flotar.

El viernes 20 de julio de 2018 a las 12:24, Robin Kruppe [email protected] escribió:

@nagisa https://github.com/nagisa Puede que estés pensando en flotar-> flotar
elencos, consulte # 15536 https://github.com/rust-lang/rust/issues/15536 y
rust-lang-nursery / nomicon # 65
https://github.com/rust-lang-nursery/nomicon/pull/65 .

-
Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/rust-lang/rust/issues/10184#issuecomment-406542903 ,
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/AApc0gA24Hz8ndnYhRXCyacd3HdUSZjYks5uIaHegaJpZM4BJ45C
.

Las notas de la versión de LLVM 7 mencionan algo:

Se mejora la optimización de los lanzamientos de punto flotante. Esto puede causar resultados sorprendentes para el código que se basa en el comportamiento indefinido de conversiones desbordadas. La optimización se puede deshabilitar especificando un atributo de función: "estricto-float-cast-overflow" = "falso". Este atributo puede ser creado por la opción clang -fno-strict-float-cast-overflow. Los desinfectantes de código se pueden utilizar para detectar patrones afectados. La opción de clang para detectar este problema por sí sola es -fsanitize = float-cast-overflow:

¿Tiene eso alguna relación con este tema?

No debería importarnos lo que LLVM hace con los lanzamientos desbordados, siempre que no sea un comportamiento indefinido inseguro. El resultado puede ser basura siempre que no pueda causar un comportamiento inadecuado.

¿Tiene eso alguna relación con este tema?

Realmente no. El UB no cambió, LLVM se volvió aún más agresivo al explotarlo, lo que hace que sea más fácil verse afectado por él en la práctica, pero el problema de la solidez no ha cambiado. En particular, el nuevo atributo no elimina el UB ni afecta las optimizaciones que existían antes de LLVM 7.

@rkruppe por curiosidad, ¿se ha quedado en el camino? Parece que https://internals.rust-lang.org/t/help-us-benchmark-saturating-float-casts/6231/14 salió lo suficientemente bien y la implementación no ha tenido demasiados errores. Parece que siempre se esperaba una ligera regresión del rendimiento, pero compilar correctamente parece una compensación que vale la pena.

¿Es esto solo esperando ser empujado a través de la línea de meta? ¿O hay otros bloqueadores conocidos?

Sobre todo, he estado distraído / ocupado con otras cosas, pero una regresión x0.82 en la codificación RBG JPEG parece más que "leve", una píldora bastante amarga de tragar (aunque es tranquilizador que otros tipos de carga de trabajo no parezcan afectados) . No es lo suficientemente severo como para oponerme a activar la saturación de forma predeterminada, pero lo suficiente como para dudar en presionar por mí mismo antes de probar el "también proporciona una función de conversión que es más rápida que la saturación pero que puede generar basura (segura) "opción discutida antes. No he llegado a eso, y aparentemente nadie más lo ha hecho, así que esto se ha quedado en el camino.

Ok genial gracias por la actualización @rkruppe! Sin embargo, tengo curiosidad por saber si realmente existe una implementación de la opción de basura segura. Podría imaginarnos fácilmente proporcionando algo como unsafe fn i32::unchecked_from_f32(...) y demás, pero parece que estás pensando que debería ser una función segura . ¿Es eso posible con LLVM hoy?

No hay freeze todavía, pero es posible usar el ensamblaje en línea para acceder a las instrucciones de la arquitectura de destino para convertir flotantes en enteros (con una alternativa a, por ejemplo, saturar as ). Si bien esto puede inhibir algunas optimizaciones, puede ser lo suficientemente bueno como para corregir la regresión en algunos puntos de referencia.

Una función unsafe que mantiene la UB sobre la que trata este problema (y está codificada de la misma manera que as es hoy) es otra opción, pero mucho menos atractiva, I ' Preferiría una función segura si puede hacer el trabajo.

También hay mucho margen de mejora en la secuencia de saturación segura de flotación a int . LLVM hoy no tiene nada específico para esto, pero si hay soluciones de ensamblaje en línea sobre la mesa, no sería difícil hacer algo como esto:

     cvttsd2si %xmm0, %eax   # x86's cvttsd2si returns 0x80000000 on overflow and invalid cases
     cmp $1, %eax            # a compact way to test whether %eax is equal to 0x80000000
     jno ok
     ...  # slow path: check for and handle overflow and invalid cases
ok:

que debería ser significativamente más rápido que lo que rustc hace actualmente .

Ok, solo quería asegurarme de aclarar, ¡gracias! Supuse que las soluciones de ensamblaje en línea no funcionan como valores predeterminados, ya que inhibirían demasiado otras optimizaciones, pero no lo he probado yo mismo. Personalmente, preferiría que cerramos este vacío defectuoso definiendo un comportamiento razonable (como exactamente los lanzamientos saturados de hoy). Si es necesario, siempre podemos preservar la implementación rápida / poco sólida de hoy como una función insegura, y en el límite de tiempo dado los recursos infinitos, podemos incluso mejorar drásticamente el valor predeterminado y / o agregar otras funciones de conversión especializadas (como una conversión segura donde no están fuera de límites 't UB pero solo un patrón de bits de basura)

¿Se opondrían otros a tal estrategia? ¿Creemos que esto no es lo suficientemente importante como para solucionarlo mientras tanto?

Creo que el ensamblaje en línea debería ser tolerable para cvttsd2si (o instrucciones similares) específicamente porque ese ensamblaje en línea no accedería a la memoria ni tendría efectos secundarios, por lo que es solo una caja negra opaca que se puede quitar si no se usa y no inhibe mucho las optimizaciones a su @sunfishcode sugiere para la saturación: las verificaciones introducidas para la saturación pueden eliminarse ocasionalmente hoy si son redundantes, pero las ramas en un bloque de asm en línea pueden t ser simplificado.

¿Se opondrían otros a tal estrategia? ¿Creemos que esto no es lo suficientemente importante como para solucionarlo mientras tanto?

No me opongo a cambiar la saturación ahora y posiblemente agregar alternativas más adelante, simplemente no quiero ser el que tenga que conseguir el consenso y justificarlo ante los usuarios cuyo código se volvió más lento 😅

Comencé a trabajar para implementar intrínsecos para saturar flotadores a int casts en LLVM: https://reviews.llvm.org/D54749

Si eso va a alguna parte, proporcionará una forma relativamente baja de obtener la semántica de saturación.

¿Cómo se reproduce este comportamiento indefinido? Probé el ejemplo en el comentario pero el resultado fue 255 , que me parece bien:

println!("{}", 1.04E+17 as u8);

El comportamiento indefinido no se puede observar de manera confiable de esa manera, a veces le da lo que espera, pero en situaciones más complejas se rompe.

En resumen, el motor de generación de código (LLVM) que usamos puede asumir que esto no sucede y, por lo tanto, puede generar un código incorrecto si alguna vez se basa en esta suposición.

@ AaronM04 se publicó un ejemplo de comportamiento indefinido reproducible en reddit hoy:

fn main() {
    let a = 360.0f32;
    println!("{}", a as u8);

    let a = 360.0f32 as u8;
    println!("{}", a);

    println!("{}", 360.0f32 as u8);
}

(ver patio de recreo )

Supongo que el último comentario fue para @ AaronM04 , en referencia a su comentario anterior .

"Oh, eso es bastante fácil entonces."

  • @pcwalton , 2014

Lo siento, he leído con mucha atención toda esta historia de 6 años de buenas intenciones. Pero, en serio, ¡¡¡6 largos de cada 10 !!! Si hubiera sido un foro político, uno habría anticipado algún sabotaje ardiente por aquí.

Entonces, por favor, ¿alguien puede explicar, en palabras simples, qué hace que el proceso de búsqueda de una solución sea más interesante que la solución en sí?

Porque es más difícil de lo que parecía inicialmente y necesita cambios de LLVM.

Ok, pero no fue Dios quien hizo este LLVM en su segunda semana, y moviéndonos en la misma dirección, podría llevar otros 15 años resolver este problema fundamental.

Realmente, no tengo atención para lastimar a alguien, y soy nuevo en la infraestructura de Rust para ayudar de repente, pero cuando me enteré de este caso, me quedé atónito.

Este rastreador de problemas es para discutir cómo resolver este problema, y ​​afirmar que lo obvio no avanza en esa dirección. Entonces, si desea ayudar a resolver el problema o tiene alguna información nueva para contribuir, hágalo, pero de lo contrario, sus comentarios no harán que aparezca mágicamente la solución. :)

Creo que la suposición de que esto requiere cambios en LLVM es prematura.

Creo que podemos hacerlo en el idioma con un costo de rendimiento mínimo. ¿Sería un cambio rotundo * * sí, pero podría hacerse, y debería hacerse?

Mi solución para ir sería definir float to int casts como unsafe luego proporcionar algunas funciones auxiliares en la biblioteca estándar para proporcionar resultados enlazados en Result Types.

Es una solución poco atractiva y es un cambio radical, pero en última instancia, es lo que cada desarrollador tiene que codificarse para trabajar en torno a la UB existente. Este es el enfoque correcto del óxido.

Gracias, @RalfJung , por hacerme entender. No tenía intención de insultar a nadie o intervenir con desdén en el productivo proceso de lluvia de ideas. Siendo nuevo en óxido, es cierto, no hay mucho que pueda hacer. Sin embargo, me ayuda a mí y tal vez a otros, que intentan entrar en el óxido, aprender más sobre sus fallas sin resolver y hacer la salida relevante: vale la pena profundizar o es mejor elegir otra cosa por ahora. Pero ya me alegro de que la eliminación de "mis comentarios inútiles" sea mucho más fácil.

Como se señaló anteriormente en el hilo, esto se está arreglando lenta pero seguramente de la manera correcta al arreglar llvm para admitir la semántica necesaria, como los equipos relevantes acordaron hace mucho tiempo.

Realmente no se puede agregar nada más a esta discusión.

https://reviews.llvm.org/D54749

@nikic Parece que el progreso por parte de LLVM se ha estancado, ¿podría dar una breve actualización si es posible? Gracias.

¿Se puede implementar el elenco de saturación como una función de biblioteca en la que los usuarios pueden optar, si están dispuestos a realizar una regresión preferida para volverse insondables? Estoy leyendo la implementación del compilador pero parece bastante sutil:

https://github.com/rust-lang/rust/blob/625451e376bb2e5283fc4741caa0a3e8a2ca4d54/src/librustc_codegen_ssa/mir/rvalue.rs#L774 -L901

Podríamos exponer un intrínseco que genera el LLVM IR para la saturación (ya sea el IR de código abierto actual o llvm.fpto[su]i.sat en el futuro) independientemente de la bandera -Z . Eso no es nada difícil de hacer.

Sin embargo, me preocupa si es el mejor curso de acción. Cuando (¿si?) La saturación se convierte en la semántica predeterminada de as casts, dicha API se vuelve redundante. Tampoco parece genial decirles a los usuarios que deben elegir por sí mismos si quieren solidez o rendimiento, incluso si es solo temporal.

Al mismo tiempo, la situación actual es claramente peor. Si estamos pensando en agregar API de biblioteca, advierto cada vez más que solo habilite la saturación de forma predeterminada y ofrezca unsafe intrínsecos que tengan UB en NaN y números fuera de rango (y menores a un simple fpto[su]i ). Eso todavía ofrecería básicamente la misma opción, pero de forma predeterminada para la solidez, y la nueva API probablemente no se volvería redundante en el futuro.

Cambiar a sonido por defecto suena bien. Creo que podemos ofrecer lo intrínseco con pereza a pedido en lugar de hacerlo desde el principio. Además, ¿const eval hará la saturación también en este caso? (cc @RalfJung @eddyb @ oli-obk)

Const eval us ya haciendo saturación y lo ha hecho durante años, creo que incluso antes de miri (recuerdo claramente haberlo cambiado en el antiguo evaluador basado en llvm::Constant ).

@rkruppe ¡Impresionante! Dado que está familiarizado con el código en cuestión, ¿le gustaría encabezar el cambio de los valores predeterminados?

@rkruppe

Podríamos exponer un intrínseco que genera el LLVM IR por saturación

Puede ser necesario que sean 10 o 12 elementos intrínsecos independientes, para cada combinación de tipo de origen y destino.

@Centril

Cambiar a sonido por defecto suena bien. Creo que podemos ofrecer lo intrínseco con pereza a pedido en lugar de hacerlo desde el principio.

Supongo que, a diferencia de otros comentarios, "lo intrínseco" en su comentario significa algo que tendría menos regresión preferida cuando as hace saturación.

No creo que este sea un buen enfoque para lidiar con regresiones significativas conocidas . Para algunos usuarios, la pérdida de rendimiento puede ser un problema real, mientras que su algoritmo asegura que la entrada esté siempre dentro del rango. Si no están suscritos a este hilo, es posible que solo se den cuenta de que se ven afectados cuando el cambio llegue al canal estable. En ese momento, es posible que se atasquen durante 6 a 12 semanas, incluso si obtenemos una API insegura inmediatamente después de la solicitud.

Preferiría que sigamos el patrón ya establecido para las advertencias de desactivación: solo haga el cambio en Nightly después de que la alternativa haya estado disponible en Stable durante algún tiempo.

Puede ser necesario que sean 10 o 12 elementos intrínsecos independientes, para cada combinación de tipo de origen y destino.

Bien, me entendiste, pero no veo cómo eso es relevante. Que sean 30 intrínsecos, todavía es trivial agregarlos. Pero en realidad, es aún más fácil tener un único intrínseco genérico utilizado por N envoltorios delgados. El número tampoco cambia si elegimos la opción "hacer as sonido e introducir una unsafe cast API".

No creo que este sea un buen enfoque para lidiar con regresiones significativas _conocidas_. Para algunos usuarios, la pérdida de rendimiento puede ser un problema real, mientras que su algoritmo asegura que la entrada esté siempre dentro del rango. Si no están suscritos a este hilo, es posible que solo se den cuenta de que se ven afectados cuando el cambio llegue al canal estable. En ese momento, es posible que se atasquen durante 6 a 12 semanas, incluso si obtenemos una API insegura inmediatamente después de la solicitud.

+1

No estoy seguro de si el procedimiento para las advertencias de desaprobación (solo desaprobar todas las noches una vez que el reemplazo es estable) es necesario, ya que parece menos importante permanecer libre de regresión de rendimiento en todos los canales de lanzamiento que permanecer libre de advertencias en todos los canales de lanzamiento. , pero, de nuevo, esperar 12 semanas más es básicamente un error de redondeo con respecto al tiempo que ha existido este problema.

También podemos dejar el -Zsaturating-float-casts alrededor (simplemente cambiando el valor predeterminado), lo que significa que cualquier usuario nocturno aún puede optar por salir del cange por un tiempo.

(Sí, el número de intrínsecos es solo un detalle de implementación y no fue un argumento a favor o en contra de nada).

@rkruppe no puede decir que ha digerido todos los comentarios aquí, pero tengo la impresión de que LLVM ahora tiene una instrucción de congelación, que era el elemento de bloqueo del "camino más corto" para la eliminación de UB aquí, ¿verdad?

Aunque supongo que freeze es tan nuevo que puede que no esté disponible en nuestra propia versión de LLVM, ¿verdad? Aún así, parece algo en lo que deberíamos explorar el desarrollo, ¿quizás durante la primera mitad de 2020?

Nominar para discusión en la reunión del compilador T, para tratar de obtener un consenso aproximado sobre nuestro camino deseado en este momento

Usar freeze sigue siendo problemático por todas las razones mencionadas aquí . No estoy seguro de cuán realistas sean estas preocupaciones con el uso de congelación para estos modelos, pero en principio se aplican. Básicamente, espere que freeze devuelva basura aleatoria o su clave secreta, lo que sea peor. (Leí esto en línea en alguna parte y realmente me gusta que tenga un resumen.: D)

Y de todos modos, incluso devolver basura aleatoria parece bastante malo para un elenco de as . Tiene sentido tener operaciones más rápidas para la velocidad donde sea necesario, similar a unchecked_add , pero hacer que el valor predeterminado parezca bastante contrario al espíritu de Rust.

@SimonSapin propuso primero el enfoque opuesto (de forma predeterminada, la semántica no es sólida / "extraña" y proporciona un método explícitamente sólido); No puedo decir a partir de sus comentarios posteriores si cree que el incumplimiento de la solidez (después de un período de transición adecuado) también es razonable / mejor.

@pnkfelix

Tengo la impresión de que LLVM ahora tiene una instrucción de congelación, que fue el elemento que bloqueó el "camino más corto" para eliminar UB aquí, ¿verdad?

Hay algunas salvedades. Lo más importante es que, incluso si lo único que nos importa es deshacernos de UB y actualizamos nuestro LLVM incluido para incluir freeze (lo que podríamos hacer en cualquier momento), admitimos varias versiones anteriores (de regreso a LLVM 6 en el momento) y necesitaríamos una implementación alternativa para que puedan deshacerse de la UB para todos los usuarios.

En segundo lugar, por supuesto, está la cuestión de si "simplemente no UB" es todo lo que nos importa mientras estamos en ello. En particular, quiero resaltar nuevamente que un freeze(fptosi %x) comporta @RalfJung ) cada vez que se ejecuta. No quiero volver a debatir esto ahora, pero vale la pena considerarlo en la reunión si preferimos hacer un poco más de trabajo para hacer que la saturación sea la conversión predeterminada y desmarcada (ya sea insegura o freeze -using) la opción no predeterminada.

@RalfJung Mi posición es que es mejor evitar as completo independientemente de este problema, porque puede tener semánticas muy diferentes (truncado, saturación, redondeo, ...) dependiendo del tipo de entrada y salida, y esas no siempre son obvio al leer el código. (Incluso lo último se puede inferir con foo as _ .) Así que tengo un borrador de RFC previo para proponer varios métodos de conversión con nombres explícitos que cubren los casos que as hace hoy (y tal vez más) .

Creo que as definitivamente no debería tener UB, ya que se puede usar fuera de unsafe . Devolver basura tampoco suena bien. Pero probablemente deberíamos tener algún tipo de mitigación / transición / alternativa para los casos conocidos de regresión del rendimiento causada por un reparto saturado. Solo pregunté sobre una implementación de biblioteca de transmisión saturada para no bloquear este borrador de RFC en esa transición.

@SimonSapin

Mi posición es que es mejor evitarlo por completo independientemente de este problema, porque puede tener semánticas muy diferentes (truncar, saturar, redondear, ...)

Convenido. Pero eso realmente no nos ayuda con este problema.

(Además, me alegra que esté trabajando para que as no sean necesarios. Espero que llegue.: D)

Creo que definitivamente no debería tener UB, ya que se puede usar fuera de lo inseguro. Devolver basura tampoco suena bien. Pero probablemente deberíamos tener algún tipo de mitigación / transición / alternativa para los casos conocidos de regresión del rendimiento causada por un reparto saturado. Solo pregunté sobre una implementación de biblioteca de transmisión saturada para no bloquear este borrador de RFC en esa transición.

¿Entonces parece que estamos de acuerdo en que el estado final debería ser que float-to-int as satura? Estoy contento con cualquier plan de transición siempre que ese sea el objetivo final hacia el que nos dirigimos.

Ese objetivo final me suena bien.

No creo que este sea un buen enfoque para lidiar con regresiones significativas _conocidas_. Para algunos usuarios, la pérdida de rendimiento puede ser un problema real, mientras que su algoritmo asegura que la entrada esté siempre dentro del rango. Si no están suscritos a este hilo, es posible que solo se den cuenta de que se ven afectados cuando el cambio llegue al canal estable. En ese momento, es posible que se atasquen durante 6 a 12 semanas, incluso si obtenemos una API insegura inmediatamente después de la solicitud.

En mi opinión, no sería el fin del mundo si esos usuarios esperaran con la actualización de su rustc durante esas 6-12 semanas; es posible que no necesiten nada de las próximas versiones en cualquier caso, o sus bibliotecas pueden tener restricciones MSRV defender.

Mientras tanto, los usuarios que tampoco están suscritos al hilo pueden tener errores de compilación, al igual que pueden sufrir pérdidas de rendimiento. ¿A cuál debemos priorizar? Damos garantías sobre la estabilidad y damos garantías sobre la seguridad, pero que yo sepa, no se dan tales garantías sobre el rendimiento (por ejemplo, RFC 1122 no menciona el rendimiento en absoluto).

Preferiría que sigamos el patrón ya establecido para las advertencias de desactivación: solo haga el cambio en Nightly después de que la alternativa haya estado disponible en Stable durante algún tiempo.

En el caso de las advertencias de depreciación, la consecuencia de esperar con la depreciación hasta que haya una alternativa estable no es, al menos hasta donde yo sé, agujeros de solidez durante el período de espera. (Además, aunque aquí se pueden proporcionar elementos intrínsecos, en el caso general, es posible que no podamos ofrecer alternativas razonables al arreglar los agujeros de solidez. Por lo tanto, no creo que tener alternativas en estable pueda ser un requisito difícil).

Bien, me entendiste, pero no veo cómo eso es relevante. Que sean 30 intrínsecos, todavía es trivial agregarlos. Pero en realidad, es aún más fácil tener un único intrínseco genérico utilizado por N envoltorios delgados. El número tampoco cambia si elegimos la opción "hacer as sonido e introducir una unsafe cast API".

¿No requerirá ese único intrínseco genérico implementaciones separadas en el compilador para esas instancias monomórficas específicas 12/30?

Puede ser trivial agregar elementos intrínsecos al compilador, porque LLVM ya ha realizado la mayor parte del trabajo, pero eso también está lejos del costo total. Además, está la implementación en Miri, Cranelift, así como el trabajo eventual necesario en una especificación. Así que no creo que debamos agregar elementos intrínsecos por si acaso alguien los necesita.

Sin embargo, no me opongo a exponer más elementos intrínsecos, pero si alguien los necesita, debería hacer una propuesta (por ejemplo, como un RP con una descripción detallada) y justificar la adición con algunos números de evaluación comparativa o algo así.

También podemos dejar el -Zsaturating-float-casts alrededor (simplemente cambiando el valor predeterminado), lo que significa que cualquier usuario nocturno aún puede optar por salir del cange por un tiempo.

Esto me parece bien, pero sugeriría cambiar el nombre de la bandera a -Zunsaturating-float-casts para evitar cambiar la semántica hacia la falta de solidez para aquellos que ya usan esta bandera.

@Centril

¿No requerirá ese único intrínseco genérico implementaciones separadas en el compilador para esas instancias monomórficas específicas 12/30?

No, la mayor parte de la implementación se puede compartir y ya se comparte mediante la parametrización de los anchos de bits de origen y destino. Solo unos pocos bits necesitan distinciones de casos. Lo mismo se aplica a la implementación en miri y muy probablemente también a otras implementaciones y especificaciones.

(Edición: para ser claros, este intercambio puede suceder incluso si hay N intrínsecos distintos, pero un solo intrínseco genérico reduce el estándar requerido por intrínseco).

Así que no creo que debamos agregar elementos intrínsecos por si acaso alguien los necesita.

Sin embargo, no me opongo a exponer más elementos intrínsecos, pero si alguien los necesita, debería hacer una propuesta (por ejemplo, como un RP con una descripción detallada) y justificar la adición con algunos números de evaluación comparativa o algo así. Mientras tanto, no creo que eso deba bloquear la reparación del agujero de solidez.

Ya tenemos algunos números de evaluación comparativa. Sabemos por la convocatoria de puntos de referencia hace mucho tiempo que la codificación JPEG se vuelve significativamente más lenta en x86_64 con @nikic ) cambiarían fundamentalmente eso. Si bien es difícil estar seguro sobre el futuro, mi conjetura es que la única forma plausible de recuperar ese rendimiento es usar algo que genere código sin verificaciones de rango, como una conversión unsafe o algo que use freeze .

De acuerdo, a partir de los números de evaluación comparativa existentes, parece que hay un deseo activo de dichos elementos intrínsecos. Si es así, propondría el siguiente plan de acción:

  1. Al mismo tiempo:

    • Introduzca los elementos intrínsecos expuestos en todas las noches a través de las funciones #[unstable(...)] .

    • Quite -Zsaturating-float-casts e introduzca -Zunsaturating-float-casts .

    • Cambie el valor predeterminado a lo que hace -Zsaturating-float-casts .

  2. Estabilizamos los intrínsecos después de un tiempo; podemos acelerar un poco.
  3. Elimina -Zunsaturating-float-casts después de un tiempo.

Suena bien. Excepto que los intrínsecos son detalles de implementación de alguna API pública, probablemente métodos en f32 y f64 . Podrían ser:

  • Métodos de un rasgo genérico (con un parámetro para el tipo de retorno entero de la conversión), opcionalmente en el preludio
  • Métodos inherentes con un rasgo de apoyo (similar a str::parse y FromStr ) para admitir diferentes tipos de devolución
  • Múltiples métodos inherentes no genéricos con el tipo de destino en el nombre

Sí, me refiero a exponer lo intrínseco a través de métodos o algo así.

Múltiples métodos inherentes no genéricos con el tipo de destino en el nombre

Esto se siente como lo que hacemos habitualmente: ¿alguna objeción a esta opción?

¿Sin embargo, lo es? Siento que cuando tengo el nombre de un tipo (de la firma) como parte del nombre de un método, son conversiones ad-hoc "únicas" (como Vec::as_slice y [T]::to_vec ) , o una serie de conversiones donde la diferencia no es un tipo (como to_ne_bytes , to_be_bytes , to_le_bytes ). Pero parte de la motivación de los rasgos de std::convert fue evitar docenas de métodos separados como u8::to_u16 , u8::to_u32 , u8::to_u64 , etc.

Me pregunto si esto sería naturalmente generalizable a un rasgo dado que los métodos deben ser unsafe fn . Si agregamos métodos inherentes, siempre puede delegar en aquellos en implementaciones de rasgos y demás.

Me parece extraño agregar rasgos para conversiones inseguras, pero supongo que probablemente Simon esté pensando en el hecho de que posiblemente necesitemos un método diferente para cada combinación de punto flotante y tipo entero (por ejemplo, f32::to_u8_unsaturated , f32::to_u16_unsaturated , etc.).

No quiero opinar sobre un hilo largo que no he leído con total ignorancia, pero ¿es eso deseado o es suficiente tener, por ejemplo, f32::to_integer_unsaturated que se convierte en u32 o algo así? ¿Existe una opción obvia para el tipo de destino para la conversión insegura?

Proporcionar conversiones inseguras solo a i32 / u32 (por ejemplo) excluye completamente todos los tipos de enteros cuyo rango de valores no es estrictamente más pequeño, y eso definitivamente a veces es necesario. A menudo, también es necesario reducir el tamaño (hasta u8, como en la codificación JPEG), pero se puede emular convirtiéndolo a un tipo entero más amplio y truncando con as (que es barato, aunque no suele ser gratis).

Pero no podemos muy bien proporcionar conversión solo al tamaño entero más grande. Esos no siempre son compatibles de forma nativa (por lo tanto, lentos) y las optimizaciones no pueden solucionarlo: no es correcto optimizar "convertir a int grande, luego truncar" en "convertir a int más pequeño directamente" porque este último tiene UB (en LLVM IR) / resultados diferentes (a nivel de código de máquina, en la mayoría de las arquitecturas) en los casos en que el resultado de la conversión original se hubiera ajustado al truncar

Tenga en cuenta que incluso excluir pragmáticamente los enteros de 128 bits y centrarse en los enteros de 64 bits seguirá siendo malo para los objetivos comunes de 32 bits.

Soy nuevo en esta conversación pero no en la programación. Tengo curiosidad por saber por qué la gente piensa que saturar las conversiones y convertir NaN a cero son comportamientos predeterminados razonables. Entiendo que Java hace esto (aunque el ajuste parece mucho más común), pero no hay un valor entero para el que realmente se pueda decir que NaN es una conversión correcta. De manera similar, convertir 1000000.0 en 65535 (u16), por ejemplo, parece incorrecto. Simplemente no hay u16 que sea claramente la respuesta correcta. Al menos, no lo veo mejor que el comportamiento actual de convertirlo a 16960, que es al menos un comportamiento compartido con C / C ++, C #, go y otros y, por lo tanto, al menos algo sorprendente.

Varias personas han comentado sobre la similitud con la verificación de desbordamiento y estoy de acuerdo con ellos. También es similar a la división de números enteros por cero. Creo que las conversiones inválidas deberían entrar en pánico al igual que la aritmética inválida. Confiar en NaN -> 0 y 1000000.0 -> 65535 (o 16960) parece tan propenso a errores como confiar en un desbordamiento de enteros o un hipotético n / 0 == 0. Es el tipo de cosas que deberían producir un error por defecto. (En las versiones de lanzamiento, rust puede eludir la verificación de errores, tal como lo hace con la aritmética de enteros). Y en los raros casos en los que _desea_ convertir NaN a cero o tener saturación de punto flotante, debería optar por hacerlo, al igual que tienes que optar por el desbordamiento de enteros.

En cuanto al rendimiento, parece que el rendimiento general más alto provendría de hacer una conversión simple y confiar en fallas de hardware. Tanto x86 como ARM, por ejemplo, generan excepciones de hardware cuando una conversión de punto flotante a entero no se puede representar correctamente (incluidos los casos NaN y fuera de rango). Esta solución es de costo cero, excepto para conversiones no válidas, excepto cuando se convierte directamente de tipo de punto flotante a entero pequeño en compilaciones de depuración, un caso raro, donde aún debería ser comparativamente barato. (En hardware teórico que no admite estas excepciones, entonces se puede emular en software, pero nuevamente solo en compilaciones de depuración). Me imagino que las excepciones de hardware son exactamente cómo se implementa hoy la detección de la división de enteros por cero. Vi mucho hablar de LLVM, por lo que tal vez esté limitado aquí, pero sería desafortunado tener emulación de software en cada conversión de punto flotante, incluso en versiones de lanzamiento, para proporcionar comportamientos alternativos dudosos para conversiones inherentemente inválidas.

@admilazz Estamos limitados por lo que LLVM puede hacer, y actualmente LLVM no expone un método para convertir de manera eficiente los flotantes en enteros sin el riesgo de un comportamiento indefinido.

La saturación se debe a que el lenguaje define las conversiones as para que siempre tengan éxito, por lo que no podemos cambiar el operador a pánico.

De manera similar, convertir 1000000.0 en 65535 (u16), por ejemplo, parece incorrecto. Simplemente no hay u16 que sea claramente la respuesta correcta. Al menos, no lo veo mejor que el comportamiento actual de convertirlo a 16960,

No fue obvio para mí, así que creo que vale la pena señalarlo: 16960 es el resultado de convertir 1000000.0 en un número entero suficientemente ancho y luego truncarlo para mantener los 16 bits más bajos.

Esta ~ no es una opción que se haya sugerido antes en este hilo, y ~ (Editar: me equivoqué aquí, lo siento no lo encontré) tampoco es el comportamiento actual. El comportamiento actual en Rust es que la conversión de flotante a entero fuera de rango es un comportamiento indefinido. En la práctica, esto a menudo conduce a un valor basura, en principio podría causar errores de compilación y vulnerabilidades. Este hilo se trata de arreglar eso. Cuando ejecuto el programa a continuación en Rust 1.39.0 obtengo un valor diferente cada vez:

fn main() {
    dbg!(1000000.0 as u16);
}

Patio de recreo . Salida de ejemplo:

[src/main.rs:2] 1000000.0 as u16 = 49072

Personalmente, creo que el truncamiento de tipo entero no es mejor ni peor que la saturación, ambos son numéricamente incorrectos para valores fuera de rango. Una conversión infalible tiene su lugar, siempre que sea determinista y no UB. Es posible que ya sepa por su algoritmo que los valores están dentro del rango, o puede que no le importen estos casos.

Creo que también deberíamos agregar API de conversión falibles que devuelvan un Result , pero aún necesito terminar de escribir ese borrador antes de RFC :)

La semántica "convertir a entero matemático, luego truncar al ancho objetivo" o "envolvente" se ha sugerido anteriormente en este hilo (https://github.com/rust-lang/rust/issues/10184#issuecomment-299229143). No me gusta particularmente:

  • Creo que es un poco menos sensible que la saturación. La saturación generalmente no da resultados razonables para números muy fuera del rango, pero:

    • se comporta de forma más sensata que el envolvente cuando los números están ligeramente fuera de rango (por ejemplo, debido a un error de redondeo acumulado). Por el contrario, un elenco que envuelve puede amplificar un error de redondeo leve en el cálculo flotante al error máximo posible en el dominio de números enteros.

    • se utiliza con cierta frecuencia en el procesamiento de señales digitales, por lo que existen al menos algunas aplicaciones en las que realmente se desea. Por el contrario, no conozco un solo algoritmo que se beneficie de la semántica envolvente.

  • AFAIK, la única razón para preferir la semántica envolvente es la eficiencia de la emulación de software, pero esto me parece una suposición no probada. Me alegraría que se demuestre que estoy equivocado, pero de un vistazo rápido, la envoltura parece requerir una cadena tan larga de instrucciones ALU (más ramas para manejar infinitos y NaN por separado) que no siento que esté claro que una será claramente mejor para rendimiento que el otro.
  • Si bien la pregunta de qué hacer con NaN es un problema desagradable para cualquier conversión a entero, la saturación al menos no requiere ninguna carcasa especial (ni en semántica ni en la mayoría de las implementaciones) para infinito. Pero para el envolvente, ¿cuál es el equivalente entero que se supone que es +/- infinito? JavaScript dice que es 0, y supongo que si hiciéramos que as entrara en pánico en NaN, entonces también podría entrar en pánico en el infinito, pero de cualquier manera esto parece que hará que el envolvente sea más difícil de hacer rápido que mirar números normales y desnormales. solo sugeriría.

Sospecho que la mayor parte del código retrocedido por la semántica de saturación para la conversión estaría mejor usando SIMD. Entonces, aunque es desafortunado, este cambio no evitará que se escriba código de alto rendimiento (especialmente si se proporciona intrínseco con semántica diferente), e incluso podría impulsar algunos proyectos hacia una implementación más rápida (aunque menos portátil).

Si es así, algunas regresiones leves de rendimiento no deben usarse como justificación para evitar cerrar un agujero de solidez.

https://github.com/rust-lang/rust/pull/66841 agrega unsafe fn métodos que convierten con fptoui y fptosi LLVM, para aquellos casos en los que se conocen los valores estar dentro del rango y saturar es una regresión de desempeño mensurable.

Después de esas tierras, creo que está bien cambiar el valor predeterminado por as (¿y quizás agregar otra bandera -Z para optar por no participar?), Aunque probablemente debería ser una decisión formal del equipo de Lang.

Después de eso, creo que está bien cambiar el valor predeterminado por as (¿y quizás agregar otra bandera -Z para optar por no participar?), Aunque probablemente debería ser una decisión formal del equipo de Lang.

Así que nosotros (el equipo de idiomas, con las personas que estaban allí al menos) discutimos esto en https://github.com/rust-lang/lang-team/blob/master/minutes/2019-11-21.md y pensamos agregar nuevos elementos intrínsecos + agregar -Zunsaturated-float-casts serían buenos primeros pasos.

Creo que sería bueno cambiar el valor predeterminado como parte de eso o poco después, posiblemente con FCP si es necesario.

Supongo que por nuevos intrínsecos te refieres a algo como https://github.com/rust-lang/rust/pull/66841

¿Qué significa agregar -Z unsaturated-float-casts sin cambiar el valor predeterminado? ¿Aceptarlo como no operativo en lugar de emitir "error: opción de depuración desconocida"?

Supongo que por nuevos intrínsecos te refieres a algo como # 66841

Sí 👍 - gracias por encabezar eso.

¿Qué significa agregar -Z unsaturated-float-casts sin cambiar el valor predeterminado? ¿Aceptarlo como no operativo en lugar de emitir "error: opción de depuración desconocida"?

Sí, básicamente. Alternativamente, eliminamos -Z saturated-float-casts a favor de -Z unsaturated-float-casts y cambiamos el valor predeterminado directamente, pero debería conducir al mismo resultado con menos RP.

Realmente no entiendo la sugerencia de "insaturados". Si el objetivo es solo proporcionar una perilla para optar por salirse del nuevo valor predeterminado, es más fácil simplemente cambiar el valor predeterminado de la bandera existente y no hacer nada más. Si el objetivo es elegir un nombre nuevo que sea más claro sobre la compensación (falta de solidez), entonces "insaturado" es terrible. En cambio, sugeriría un nombre que incluya "inseguro" o "UB" o un nombre similar. palabra de miedo, por ejemplo -Z fix-float-cast-ub .

unchecked es el término con algún precedente en los nombres de API.

@admilazz Estamos limitados por lo que LLVM puede hacer, y actualmente LLVM no expone un método para convertir de manera eficiente los flotantes en enteros sin el riesgo de un comportamiento indefinido.

Pero presumiblemente puede agregar verificaciones de tiempo de ejecución solo en compilaciones de depuración, como lo hace para el desbordamiento de enteros.

AFAIK, la única razón para preferir la semántica envolvente es la eficiencia de la emulación de software

No creo que debamos preferir el envolvente o la saturación, ya que ambos están mal, pero el envolvente al menos tiene la ventaja de ser el método utilizado por muchos lenguajes similar a rust: C / C ++, C #, go, probablemente D, y seguramente más, y de ser también el comportamiento actual de rust (al menos a veces). Dicho esto, creo que "entrar en pánico en conversiones no válidas (posiblemente solo en compilaciones de depuración)" es ideal, al igual que lo hacemos para el desbordamiento de enteros y la aritmética no válida como la división por cero.

(Curiosamente, obtuve 16960 en el patio de recreo . Pero veo en otros ejemplos publicados que a veces el óxido lo hace de manera diferente ...)

La saturación se debe a que el lenguaje se define como conversiones para tener éxito siempre, por lo que no podemos cambiar el operador para que entre en pánico.

Cambiar lo que evalúa la operación ya es un cambio radical, en la medida en que nos preocupan los resultados de las personas que ya lo están haciendo. Este comportamiento sin pánico también podría cambiar.

Supongo que si entramos en pánico en NaN, entonces también podría entrar en pánico en el infinito, pero de cualquier manera esto parece que hará que el envolvente sea más difícil de hacer rápido.

Si solo está marcado en compilaciones de depuración, como lo es el desbordamiento de enteros, entonces creo que podemos obtener lo mejor de ambos mundos: se garantiza que las conversiones sean correctas (en compilaciones de depuración), es más probable que se detecten los errores del usuario, puede optar por a comportamientos extraños como envolvente y / o saturación si lo desea, y el rendimiento es tan bueno como puede ser.

Además, parece extraño controlar estas cosas a través de un interruptor de línea de comandos. Eso es un gran martillo. Sin duda, el comportamiento deseado de una conversión fuera de rango depende de las especificaciones del algoritmo, por lo que es algo que debe controlarse por conversión. Sugeriría f.to_u16_sat () y f.to_u16_wrap () o similar como opt-ins, y no tener ninguna opción de línea de comandos que cambie la semántica del código. Eso dificultaría la combinación y combinación de diferentes piezas de código, y no puedes entender qué hace algo al leerlo ...

Y, si es realmente inaceptable hacer que "pánico si no es válido" sea el comportamiento predeterminado, sería bueno tener un método intrínseco que lo implemente pero que solo realice la verificación de validez en las compilaciones de depuración para que podamos asegurarnos de que nuestras conversiones sean correctas en el (vasto ¿La mayoría de?) casos en los que esperamos obtener el mismo número después de la conversión, pero sin pagar ninguna penalización en las versiones de lanzamiento.

Curiosamente, obtuve 16960 en el patio de recreo.

Así es como funciona Undefined Behavior: dependiendo de la formulación exacta del programa y la versión exacta del compilador y los indicadores de compilación exactos, es posible que obtenga un comportamiento determinista, o un valor basura que cambia en cada ejecución, o errores de compilación. El compilador puede hacer cualquier cosa.

Wraparound al menos tiene la ventaja de ser el método utilizado por muchos lenguajes similar a rust: C / C ++, C #, go, probablemente D, y seguramente más

¿De verdad? Al menos no en C y C ++, tienen el mismo comportamiento indefinido que Rust. Esto no es una coincidencia, usamos LLVM que está construido principalmente para clang implementando C y C ++. ¿Estás seguro de C # y listo?

Estándar C11 https://port70.net/~nsz/c/c11/n1570.html#6.3.1.4

Cuando un valor finito de tipo flotante real se convierte en un tipo entero distinto de _Bool, la parte fraccionaria se descarta (es decir, el valor se trunca hacia cero). Si el valor de la parte integral no puede ser representado por el tipo entero, el comportamiento es indefinido.

La operación restante realizada cuando un valor de tipo entero se convierte en tipo sin signo no necesita realizarse cuando un valor de tipo flotante real se convierte en tipo sin signo. Por lo tanto, el rango de valores flotantes reales portátiles es (-1, Utype_MAX + 1).

Estándar C ++ 17 http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4659.pdf#section.7.10

Un prvalue de tipo de coma flotante se puede convertir en un prvalue de tipo entero. La conversión se trunca; es decir, se descarta la parte fraccionaria. El comportamiento no está definido si el valor truncado no se puede representar en el tipo de destino.

Referencia de C # https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/numeric-conversions

Cuando convierte un valor doble o flotante en un tipo integral, este valor se redondea hacia cero al valor integral más cercano. Si el valor integral resultante está fuera del rango del tipo de destino, el resultado depende del contexto de verificación de desbordamiento. En un contexto verificado, se lanza una OverflowException, mientras que en un contexto no verificado, el resultado es un valor no especificado del tipo de destino.

Entonces no es UB, solo un "valor no especificado".

@admilazz Hay una gran diferencia entre esto y el desbordamiento de enteros: el desbordamiento de enteros no es deseable pero está bien definido . Los lanzamientos de punto flotante son comportamientos indefinidos .

Lo que está pidiendo es similar a desactivar la comprobación de límites de Vec en el modo de liberación, pero eso sería incorrecto porque permitiría un comportamiento indefinido.

Permitir un comportamiento indefinido en código seguro no es aceptable, incluso si solo ocurre en el modo de lanzamiento. Por lo tanto, cualquier corrección debe aplicarse tanto al modo de liberación como al modo de depuración.

Por supuesto, es posible tener una solución más restrictiva en el modo de depuración, pero la solución para el modo de lanzamiento aún debe estar bien definida.

@admilazz Hay una gran diferencia entre esto y el desbordamiento de enteros: el desbordamiento de enteros no es deseable pero está bien definido. Los lanzamientos de punto flotante son un comportamiento indefinido.

Claro, pero este hilo trata de definir el comportamiento. Si se definiera como la producción de "un valor no especificado del tipo de destino", como en la especificación de C # que Amanieu hizo referencia anteriormente, entonces ya no estaría indefinido (de ninguna manera peligrosa). No puede hacer uso fácilmente de la naturaleza bien definida del desbordamiento de enteros en programas prácticos porque aún entrará en pánico en las compilaciones de depuración. De manera similar, el valor producido por una conversión no válida en las versiones de lanzamiento no tiene por qué ser predecible o particularmente útil porque los programas no podrían prácticamente hacer uso de él de todos modos si entrara en pánico en las versiones de depuración. En realidad, esto le da al compilador el máximo alcance para las optimizaciones, mientras que elegir un comportamiento como la saturación restringe el compilador y podría ser significativamente más lento en hardware sin instrucciones de conversión nativas de saturación. (Y no es que la saturación sea claramente correcta).

Lo que está pidiendo es similar a desactivar la comprobación de los límites de Vec en el modo de lanzamiento, pero eso sería incorrecto porque permitiría un comportamiento indefinido. Permitir un comportamiento indefinido en un código seguro no es aceptable ...

No todos los comportamientos indefinidos son iguales. El comportamiento indefinido solo significa que depende del implementador del compilador decidir qué sucede. Siempre que no haya forma de violar las garantías de seguridad de rust lanzando un flotador a un int, entonces no creo que sea similar a permitir que las personas escriban en ubicaciones de memoria arbitrarias. No obstante, por supuesto, estoy de acuerdo en que debería definirse en el sentido de garantizar la seguridad, aunque no sea necesariamente predecible.

¿De verdad? Al menos no en C y C ++, tienen el mismo comportamiento indefinido que Rust ... ¿Estás seguro de C # y listo?

Lo suficientemente justo. No leí todas sus especificaciones; Acabo de probar varios compiladores. Tienes razón en que decir "todos los compiladores que probé lo hacen de esta manera" es diferente de decir "las especificaciones del lenguaje lo definen de esta manera". Pero no estoy argumentando a favor del desbordamiento de todos modos, solo señalo que parece ser el más común. Realmente estoy argumentando a favor de tener una conversión que 1) proteja contra resultados "incorrectos" como que 1000000.0 se convierta en 65535 o 16960 por la misma razón por la que protegemos contra el desbordamiento de enteros; lo más probable es que sea un error, por lo que los usuarios deberían optar por participar. y 2) permite el máximo rendimiento en las versiones de lanzamiento.

No todos los comportamientos indefinidos son iguales. El comportamiento indefinido solo significa que depende del implementador del compilador decidir qué sucede. Siempre que no haya forma de violar las garantías de seguridad de rust lanzando un flotador a un int, entonces no creo que sea similar a permitir que las personas escriban en ubicaciones de memoria arbitrarias. No obstante, por supuesto que estoy de acuerdo en que debería definirse: definido, pero no necesariamente predecible.

El comportamiento indefinido significa que los optimizadores (que son proporcionados por desarrolladores de LLVM centrados en C y C ++) son libres de asumir que nunca puede suceder y transformar el código en función de esa suposición, incluida la eliminación de fragmentos de código que solo son accesibles pasando por el elenco indefinido o, como muestra este ejemplo , suponiendo que se debe haber llamado a una asignación, aunque en realidad no lo fue, porque invocar el código que se llama sin llamarlo primero sería un comportamiento indefinido.

Incluso si fuera razonable demostrar que componer los diferentes pases de optimización no produce comportamientos emergentes peligrosos, los desarrolladores de LLVM no harán ningún esfuerzo consciente para preservar eso.

Yo diría que todo comportamiento indefinido es similar sobre esa base.

Incluso si fuera razonable demostrar que componer los diferentes pases de optimización no produce comportamientos emergentes peligrosos, los desarrolladores de LLVM no harán ningún esfuerzo consciente para preservar eso.

Bueno, es desafortunado que LLVM incida en el diseño de Rust de esta manera, pero acabo de leer algunas de las referencias de instrucciones de LLVM y menciona la operación "congelar" mencionada anteriormente ("... otra es esperar a que LLVM agregue un congelamiento concepto ... ") que evitaría un comportamiento indefinido en el nivel LLVM. ¿El óxido está ligado a una versión antigua de LLVM? Si no, podríamos usarlo. Sin embargo, su documentación no está clara sobre el comportamiento exacto.

Si el argumento es indefinido o venenoso, 'congelar' devuelve un valor arbitrario, pero fijo, de tipo 'ty'. De lo contrario, esta instrucción no es operativa y devuelve el argumento de entrada. Todos los usos de un valor devuelto por la misma instrucción 'congelar' están garantizados para observar siempre el mismo valor, mientras que diferentes instrucciones 'congelar' pueden producir valores diferentes.

No sé qué quieren decir con "valor fijo" o "la misma instrucción de 'congelación'". Creo que idealmente se compilaría en un no-op y daría un entero impredecible, pero parece que posiblemente podría hacer algo costoso. ¿Alguien ha probado esta operación de congelación?

Bueno, es lamentable que LLVM incida en el diseño de Rust de esta manera

No es solo que los desarrolladores de LLVM escriban los optimizadores. Es que, incluso si los desarrolladores de rustc escribieron los optimizadores, coquetear con la indefinición es intrínsecamente un arma enorme debido a las propiedades emergentes de encadenar optimizadores. El cerebro humano simplemente no evolucionó para "intuir la magnitud potencial del error de redondeo" cuando el redondeo en cuestión es un comportamiento emergente creado al encadenar pases de optimización.

No voy a estar en desacuerdo contigo en eso. :-) Espero que esta instrucción de "congelación" de LLVM proporcione una forma sin costo de evitar este comportamiento indefinido.

Eso se discutió anteriormente y la conclusión fue que si bien emitir y luego congelar es un comportamiento un comportamiento as .

En mi opinión, tal semántica sería un mal diseño de lenguaje que preferiríamos evitar.

Mi posición es que es mejor evitar as completo independientemente de este problema, porque puede tener semánticas tremendamente diferentes (truncado, saturación, redondeo, ...) dependiendo del tipo de entrada y salida, y esas no siempre son obvias cuando código de lectura. (Incluso lo último se puede inferir con foo as _ .) Así que tengo un borrador de RFC previo para proponer varios métodos de conversión con nombres explícitos que cubren los casos que as hace hoy (y tal vez más) .

¡Terminé ese borrador! https://internals.rust-lang.org/t/pre-rfc-add-explicitly-named-numeric-conversion-apis/11395

Cualquier comentario es muy bienvenido, pero por favor déjelo en los hilos internos en lugar de aquí.

En el modo de lanzamiento, tales conversiones devolverían resultados arbitrarios para entradas fuera del límite (en un código completamente seguro). Esa no es una buena semántica para algo tan inocente como.

Lamento repetirme, pero creo que este mismo argumento se aplica al desbordamiento de enteros. Si multiplica algunos números y el resultado se desborda, obtendrá un resultado tremendamente incorrecto que casi con seguridad invalidará el cálculo que estaba tratando de realizar, pero entra en pánico en las compilaciones de depuración y, por lo tanto, es probable que se detecte el error. Yo diría que una conversión numérica que da resultados tremendamente incorrectos también debería entrar en pánico porque hay una gran probabilidad de que represente un error en el código del usuario. (El caso de inexactitud típica de punto flotante ya se ha manejado. Si un cálculo produce 65535.3 ya es válido convertirlo a u16. Para obtener una conversión fuera de límites, normalmente necesita un error en su código, y si tengo un error Quiero ser notificado para poder solucionarlo).

La capacidad de las versiones de versiones para dar resultados arbitrarios pero definidos para conversiones no válidas también permite el máximo rendimiento, que es importante, en mi opinión, para algo tan fundamental como las conversiones numéricas. La saturación siempre tiene un impacto significativo en el rendimiento, oculta errores y rara vez realiza un cálculo que inesperadamente lo encuentra y da el resultado correcto.

Lamento repetirme, pero creo que este mismo argumento se aplica al desbordamiento de enteros. Si multiplica algunos números y el resultado se desborda, obtendrá un resultado tremendamente incorrecto que casi con seguridad invalidará el cálculo que estaba tratando de realizar.

Sin embargo, no estamos hablando de multiplicación, estamos hablando de moldes. Y sí, lo mismo se aplica al desbordamiento de enteros: las conversiones de int-a-int nunca entran en pánico, incluso cuando se desbordan. Esto se debe a que as , por diseño, nunca entra en pánico, ni siquiera en las compilaciones de depuración. Desviarse de esto para los lanzamientos de punto flotante es sorprendente en el mejor de los casos y peligroso en el peor, ya que la exactitud y la seguridad de un código inseguro pueden depender de que ciertas operaciones no entren en pánico.

Si quiere argumentar que el diseño de as es defectuoso porque proporciona una conversión infalible entre tipos donde la conversión adecuada no siempre es posible, creo que la mayoría de nosotros estará de acuerdo. Pero eso está completamente fuera del alcance de este hilo, que trata de corregir las conversiones flotantes a int dentro del marco existente de as casts . Estos tienen que ser infalibles, no deben entrar en pánico, ni siquiera en las versiones de depuración. Por lo tanto, proponga una semántica razonable (que no implique freeze ) y sin pánico para conversiones flotantes a int, o intente iniciar una nueva discusión sobre el rediseño de as para permitir el pánico cuando el elenco tiene pérdida (y hágalo de manera consistente para conversiones de int-a-int y float-to-int), pero este último está fuera de tema en este problema, así que abra un nuevo hilo (estilo pre-RFC) para eso.

¿Qué tal si comenzamos simplemente implementando la semántica freeze ahora para arreglar el UB, y luego podemos tener todo el tiempo del mundo para acordar qué semántica realmente queremos, ya que cualquier semántica que elijamos será compatible con freeze semántica.

¿Qué tal si comenzamos simplemente implementando freeze semántica _ahora_ para arreglar el UB, y luego podemos tener todo el tiempo del mundo para acordar qué semántica realmente queremos ya que cualquier semántica que elijamos será compatible con freeze semántica.

  1. El pánico no es retrocompatible con la congelación, por lo que tendríamos que rechazar al menos todas las propuestas que implican entrar en pánico. Pasar de UB a entrar en pánico es menos obviamente incompatible, aunque, como se mencionó anteriormente, hay otras razones para no hacer que as pánico.
  2. Como escribí antes ,
    > Admitimos varias versiones anteriores (de nuevo a LLVM 6 en este momento) y necesitaríamos alguna implementación alternativa para que puedan deshacerse de la UB para todos los usuarios.

Estoy de acuerdo con @RalfJung en que hacer que solo algunos as entren en pánico es altamente indeseable, pero aparte de eso, no creo que este punto que @admilazz hizo sea obviamente correcto:

(El caso de inexactitud típica de punto flotante ya se ha manejado. Si un cálculo produce 65535.3 ya es válido convertirlo a u16. Para obtener una conversión fuera de límites, normalmente necesita un error en su código, y si tengo un error Quiero ser notificado para poder solucionarlo).

Para f32-> u16 puede ser cierto que necesita un error de redondeo extraordinariamente grande para salirse del rango de u16 solo por error de redondeo, pero para las conversiones de números enteros de f32 a 32 bits eso no es tan obviamente cierto. i32::MAX no se puede representar exactamente en f32, el número representable más cercano es 47 de i32::MAX . Entonces, si tiene un cálculo que debería resultar matemáticamente en un número de hasta i32::MAX , cualquier error> = 1 ULP de cero lo pondrá fuera de los límites. Y se pone mucho peor una vez que consideramos flotantes de menor precisión (IEEE 754 binary16, o el bfloat16 no estándar).

Sin embargo, no estamos hablando de multiplicación, estamos hablando de moldes

Bueno, las conversiones de punto flotante a entero se usan casi exclusivamente en el mismo contexto que la multiplicación: cálculos numéricos, y creo que hay un paralelismo útil con el comportamiento del desbordamiento de enteros.

Y sí, lo mismo se aplica al desbordamiento de enteros: las conversiones de int-a-int nunca entran en pánico, incluso cuando se desbordan ... Desviarse de esto para las conversiones de punto flotante es sorprendente en el mejor de los casos y peligroso en el peor, ya que la corrección y la seguridad en el código inseguro puede depender de que ciertas operaciones no entren en pánico.

Yo diría que la inconsistencia aquí está justificada por la práctica común y no sería tan sorprendente. Truncar y dividir enteros con cambios, máscaras y conversiones, utilizando efectivamente conversiones como una forma de bit a bit Y más un cambio de tamaño, es muy común y tiene una larga historia en la programación de sistemas. Es algo que hago al menos varias veces a la semana. Pero durante los últimos 30 años, no recuerdo haber esperado obtener un resultado razonable al convertir NaN, Infinity o un valor de punto flotante fuera de rango en un número entero. (Cada instancia de eso que puedo recordar ha sido un error en el cálculo que produjo el valor). Por lo tanto, no creo que el caso de números enteros -> conversiones de enteros y punto flotante -> conversiones de enteros deban tratarse de manera idéntica. Dicho esto, puedo entender que algunas decisiones ya se hayan grabado en piedra.

por favor ... proponga una semántica razonable (que no implique la congelación) y sin pánico para las conversiones flotantes a int

Bueno, mi propuesta es:

  1. No utilice modificadores de compilación global que efectúen cambios significativos en la semántica. (Supongo que -Zsaturating-float-casts es un parámetro de línea de comandos o similar). El código que depende del comportamiento de saturación, por ejemplo, se rompería si se compilara sin él. Presumiblemente, el código con diferentes expectativas no se puede mezclar en el mismo proyecto. Debería haber alguna forma local para que un cálculo especifique la semántica deseada, probablemente algo como este pre-RFC .
  2. Hacer que los elencos as tengan el máximo rendimiento por defecto, como se esperaría de un elenco.

    • Creo que esto debería hacerse mediante la congelación en las versiones LLVM que lo admiten y cualquier otra semántica de conversión en versiones LLVM que no lo hacen (por ejemplo, truncamiento, saturación, etc.). Espero que la afirmación de que "la congelación podría filtrar valores de la memoria sensible" sea puramente hipotética. (O, si y = freeze(fptosi(x)) simplemente deja y sin cambios, por lo que se pierde memoria no inicializada, eso podría solucionarse borrando y primero).

    • Si as será relativamente lento de forma predeterminada (por ejemplo, porque se satura), proporcione alguna forma de obtener el máximo rendimiento (por ejemplo, un método - inseguro si es necesario - que use congelación).

  1. No utilice modificadores de compilación global que efectúen cambios significativos en la semántica. (Supongo que -Zsaturating-float-casts es un parámetro de línea de comandos o similar).

Para ser claros, no creo que nadie esté en desacuerdo. Esta bandera solo se propuso como una herramienta a corto plazo para medir y solucionar más fácilmente las regresiones de rendimiento mientras las bibliotecas se actualizan para corregir esas regresiones.

Para f32-> u16 puede ser cierto que necesita un error de redondeo extraordinariamente grande para salirse del rango de u16 solo por error de redondeo, pero para las conversiones de números enteros de f32 a 32 bits eso no es tan obviamente cierto. i32 :: MAX no se puede representar exactamente en f32, el número representable más cercano es 47 de i32 :: MAX. Entonces, si tiene un cálculo que matemáticamente debería resultar en un número hasta i32 :: MAX, cualquier error> = 1 ULP de cero lo pondrá fuera de los límites

Esto se está saliendo un poco del tema, pero digamos que tiene este algoritmo hipotético que se supone que produce matemáticamente f32s hasta 2 ^ 31-1 (pero _no_ debería producir 2 ^ 31 o superior, excepto posiblemente debido a un error de redondeo). Ya parece tener fallas.

  1. Creo que el i32 representable más cercano es en realidad 127 por debajo de i32 :: MAX, por lo que incluso en un mundo perfecto sin imprecisión de punto flotante, el algoritmo que espera producir valores de hasta 2 ^ 31-1 solo puede producir (legal ) valores hasta 2 ^ 31-128. Quizás eso ya sea un error. No estoy seguro de que tenga sentido hablar del error medido desde 2 ^ 31-1 cuando ese número no es posible de representar. Tendría que alejarse 64 del número representable más cercano (considerando el redondeo) para salir de los límites. Por supuesto, eso no es mucho porcentual cuando estás cerca de 2 ^ 32.
  2. No debe esperar discriminación de valores que están separados por 1 (es decir, 2 ^ 31-1 pero no 2 ^ 31) cuando los valores representables más cercanos están separados por 128. Además, solo el 3,5% de los i32 se pueden representar como f32 (y <2% de los u32). No puede obtener ese tipo de rango y al mismo tiempo tener ese tipo de precisión con un f32. Parece que el algoritmo está usando la herramienta incorrecta para el trabajo.

Supongo que cualquier algoritmo práctico que haga lo que describe estará íntimamente ligado a los números enteros de alguna manera. Por ejemplo, si convierte un i32 aleatorio a f32 y viceversa, puede fallar si está por encima de i32 :: MAX-64. Pero eso degrada mucho tu precisión y no sé por qué harías tal cosa. Prácticamente cualquier cálculo de i32 -> f32 -> i32 que genera el rango completo de i32 se puede expresar más rápido y con mayor precisión con matemáticas enteras, y si no, hay f64.

De todos modos, aunque estoy seguro de que es posible encontrar algunos casos en los que los algoritmos que realizan conversiones fuera de límites se corrijan por saturación, creo que son raros, lo suficientemente raros como para no ralentizar _todas_ las conversiones para acomodarlas. . Y yo diría que estos algoritmos probablemente todavía tienen fallas y deberían corregirse. Y si un algoritmo no se puede arreglar, siempre puede hacer una verificación de límites antes de la conversión posiblemente fuera de límites (o llamar a una función de conversión de saturación). De esa forma, el gasto de limitar el resultado se paga solo cuando sea necesario.

PD: Feliz Día de Acción de Gracias tardío para todos.

Para ser claros, no creo que nadie esté en desacuerdo. Esta bandera solo se propuso como una herramienta a corto plazo ...

Me refería principalmente a la propuesta de reemplazar -Zsaturated-float-casts con -Zinsaturated-float-casts. Incluso si la saturación se convierte en el valor predeterminado, los indicadores como -Zinsaturated-float-casts parecen malos para la compatibilidad, pero si también se pretende que sea temporal, entonces está bien, no importa. :-)

De todos modos, estoy seguro de que todos esperan haber dicho lo suficiente sobre este tema, incluido yo mismo. Sé que el equipo de Rust ha buscado tradicionalmente proporcionar múltiples formas de hacer las cosas para que las personas puedan tomar sus propias decisiones entre rendimiento y seguridad. Compartí mi perspectiva y confío en que ustedes, al final, encontrarán una buena solución. ¡Cuídate!

Supuse que -Zunsaturated-float-casts solo existiría temporalmente y se eliminaría en algún momento. Que es una opción -Z (solo disponible en Nightly) en lugar de -C sugiere, al menos.

Por lo que vale, la saturación y la UB no son las únicas opciones. Otra posibilidad es cambiar LLVM para agregar una variante de fptosi que use el comportamiento de desbordamiento nativo de la CPU, es decir, el comportamiento de desbordamiento no sería portátil entre arquitecturas, pero estaría bien definido en cualquier arquitectura dada ( por ejemplo, devolviendo 0x80000000 en x86), y nunca devolvería veneno o memoria no inicializada. Incluso si el valor predeterminado se vuelve saturado, sería bueno tenerlo como una opción. Después de todo, mientras que las conversiones de saturación tienen una sobrecarga inherente en arquitecturas donde no son el comportamiento predeterminado, "hacer lo que hace la CPU" solo tiene sobrecarga si inhibe alguna optimización específica del compilador. No estoy seguro, pero sospecho que cualquier optimización habilitada al tratar el desbordamiento de flotante a int como UB es un nicho y no se aplica a la mayoría de los códigos.

Dicho esto, un problema podría ser si una arquitectura tiene varias instrucciones flotantes a int que devuelven diferentes valores en el desbordamiento. En este caso, el compilador que elige uno u otro afectaría el comportamiento observable, que no es un problema en sí mismo, pero podría convertirse en uno si se duplica un solo fptosi y las dos copias terminan comportándose de manera diferente. Pero no estoy seguro de si este tipo de divergencia existe realmente en arquitecturas populares. Y el mismo problema se aplica a otras optimizaciones de punto flotante , incluida la

const fn (miri) ya ha elegido el comportamiento de transmisión saturada desde Rust 1.26 (asumiendo que queremos que el resultado CTFE y RTFE sea consistente) (antes de 1.26, la transmisión en tiempo de compilación desbordante devuelve 0)

const fn g(a: f32) -> i32 {
    a as i32
}

const Q: i32 = g(1e+12);

fn main() {
    println!("{}", Q); // always 2147483647
    println!("{}", g(1e+12)); // unspecified value, but always 2147483647 in miri
}

Miri / CTFE usa los métodos to_u128 / to_i128 apfloat para realizar la conversión. Pero no estoy seguro de si se trata de una garantía estable, dado que, en particular, parece haber cambiado antes (de lo que no estábamos al tanto al implementar esas cosas en Miri).

Creo que podríamos ajustar esto a cualquier codegen que termine recogiendo. Pero el hecho de que apfloat de LLVM (del cual la versión Rust es un puerto directo) usa saturación es un buen indicador de que se trata de una especie de "valor predeterminado razonable".

Una solución al comportamiento observable podría ser elegir aleatoriamente uno de los métodos disponibles en el momento de la compilación del compilador o el binario resultante.
Luego tenga funciones como a.saturating_cast::<i32>() para usuarios que requieran un comportamiento específico.

@ dns2utf8

La palabra "aleatoriamente" iría en contra del esfuerzo por obtener compilaciones reproducibles y, si es predecible dentro de una versión del compilador, sabes que alguien decidirá que dependerá de que no cambie.

En mi opinión, lo que describió @comex (no es novedoso para este hilo IIRC, todo lo viejo es nuevo nuevamente) esta es la siguiente mejor opción si no queremos saturación. Tenga en cuenta que ni siquiera necesitamos ningún cambio de LLVM para probarlo, podemos usar asm en línea (en arquitecturas donde existen tales instrucciones).

Dicho esto, un problema podría ser si una arquitectura tiene varias instrucciones flotantes a int que devuelven diferentes valores en el desbordamiento. En este caso, el compilador que elige uno u otro afectaría el comportamiento observable, que no es un problema en sí mismo, pero podría convertirse en uno si se duplica un solo fptosi y las dos copias terminan comportándose de manera diferente.

En mi opinión, tal no determinismo renunciaría a casi todas las ventajas prácticas en comparación con freeze . Si hacemos esto, deberíamos elegir una instrucción por arquitectura y ceñirnos a ella, tanto por determinismo como para que los programas puedan realmente confiar en el comportamiento de la instrucción cuando tenga sentido para ellos. Si esto no es posible en alguna arquitectura, entonces podríamos recurrir a una implementación de software (pero como usted dice, esto es completamente hipotético).

Esto es más fácil si no delegamos esta decisión a LLVM, sino que implementamos la operación con asm en línea. Lo cual, por cierto, también sería mucho más fácil que cambiar LLVM para agregar nuevos intrínsecos y reducirlos en cada backend.

@rkruppe

[...] Lo cual, de manera incidental, también sería mucho más fácil que cambiar LLVM para agregar nuevos intrínsecos y reducirlos en cada backend.

Además, LLVM no está exactamente contento con los intrínsecos con semántica dependiente del objetivo:

Sin embargo, si desea que los elencos estén bien definidos, debe definir su comportamiento. "Hacer algo rápido" no es realmente una definición, y no creo que debamos dar un comportamiento dependiente del objetivo a construcciones independientes del objetivo.

https://groups.google.com/forum/m/#!msg/llvm -dev / cgDFaBmCnDQ / CZAIMj4IBAA

Voy retag # 10184 exclusivamente como T-lang: Creo que los problemas que deben resolverse hay opciones semánticas sobre qué float as int medios

(es decir, si estamos dispuestos a permitir que tenga una semántica de pánico o no, si estamos dispuestos a permitir que tenga una subespecificación basada en freeze o no, etc.)

estas son preguntas mejor dirigidas al equipo de T-lang, no al compilador de T, al menos para la discusión inicial, OMI

Me encontré con este problema produciendo resultados que son _ irreproducibles entre ejecuciones_ incluso sin volver a compilar. El operador as parece buscar algo de basura de la memoria en tales casos.

Sugiero simplemente no permitir completamente el uso de as para "flotar como int" y confiar en métodos de redondeo específicos. Razonamiento: as no tiene pérdidas para otros tipos.

Razonamiento: como no tiene pérdidas para otros tipos.

¿Lo es?

Según Rust Book, puedo suponer que no tiene pérdidas solo en ciertos casos (es decir, en los casos en que From<X> se define para un tipo Y), es decir, puede emitir u8 a u32 usando From , pero no al revés.

Por "sin pérdidas" me refiero a la emisión de valores que son lo suficientemente pequeños para encajar. Ejemplo: 1_u64 as u8 no tiene pérdidas, por lo tanto u8 as u64 as u8 no tiene pérdidas. Para los flotadores no existe una definición simple de "ajustes" ya que 20000000000000000000000000000_u128 as f32 no tiene pérdida mientras que 20000001_u32 as f32 , así que ni float as int ni int as float tienen pérdida.

256u64 as u8 embargo,

Pero <anything>_u8 as u64 as u8 no lo es.

Creo que la pérdida es normal y se espera con yesos, y no es un problema. Truncar enteros con conversiones (por ejemplo, u32 as u8 ) es una operación común con un significado bien entendido que es consistente en todos los lenguajes similares a C que conozco (al menos en arquitecturas que usan representaciones de enteros en complemento a dos, que es básicamente todos ellos en estos días). Las conversiones de punto flotante válidas (es decir, donde la parte integral encaja en el destino) también tienen una semántica bien entendida y acordada. 1.6 as u32 tiene pérdidas, pero todos los lenguajes de tipo C que conozco están de acuerdo en que el resultado debería ser 1. Ambos casos se derivan del consenso entre los fabricantes de hardware sobre cómo deberían funcionar esas conversiones y la convención en C -como los lenguajes que emiten deben ser de alto rendimiento, tipo de operadores "Sé lo que estoy haciendo".

Por lo tanto, no creo que debamos considerarlos problemáticos de la misma manera que las conversiones de punto flotante no válidas, ya que no tienen ninguna semántica acordada en lenguajes similares a C o en hardware (pero generalmente dan como resultado estados de error o excepciones de hardware) y casi siempre indican errores (en mi experiencia) y, por lo tanto, generalmente no existen en el código correcto.

Me encontré con este problema produciendo resultados que son irreproducibles entre ejecuciones, incluso sin volver a compilar. El operador as parece buscar algo de basura de la memoria en tales casos.

Personalmente, creo que está bien siempre y cuando solo ocurra cuando la conversión no sea válida y no tenga ningún efecto secundario además de producir un valor basura. Si realmente necesita una conversión inválida en un fragmento de código, puede manejar el caso inválido usted mismo con la semántica que crea que debería tener.

y no tiene efectos secundarios además de producir valor basura

El efecto secundario es que el valor basura se origina en algún lugar de la memoria y revela algunos datos (posiblemente confidenciales). Devolver un valor "aleatorio" calculado únicamente a partir de flotante estaría bien, pero el comportamiento actual no lo es.

Las conversiones de punto flotante válidas (es decir, donde la parte integral encaja en el destino) también tienen una semántica bien entendida y acordada.

¿Existen casos de uso de conversiones flotantes a int que no estén acompañadas de trunc() , round() , floor() o ceil() explícitos? La estrategia de redondeo actual de as es "indefinida", por lo que as apenas se puede utilizar para números no redondeados. Creo que en la mayoría de los casos el que escribe x as u32 realmente quiere x.round() as u32 .

Creo que la pérdida es normal y se espera con yesos, y no es un problema.

Estoy de acuerdo, pero solo si la pérdida es fácilmente predecible. Para enteros, las condiciones de conversión con pérdida son obvias. Para los flotadores son oscuros. No tienen pérdidas para algunos números muy grandes, pero tienen pérdidas para algunos más pequeños, incluso si son redondos. Mi preferencia personal es tener dos operadores diferentes para conversiones con pérdida y sin pérdida para evitar introducir una conversión con pérdida por error, pero también estoy bien con un solo operador siempre que pueda saber si tiene pérdida o no.

El efecto secundario es que el valor basura se origina en algún lugar de la memoria y revela algunos datos (posiblemente confidenciales).

Esperaría que dejara el destino sin cambios o lo que sea, pero si eso es realmente un problema, primero se podría poner a cero.

¿Hay casos de uso de conversiones flotantes a int que no estén acompañadas de trunc (), round (), floor () o ceil () explícitos? La estrategia de redondeo actual de as es "indefinida", lo que hace que sea apenas utilizable para números no redondeados.

Si la estrategia de redondeo es realmente indefinida, entonces sería una sorpresa para mí, y estoy de acuerdo en que el operador apenas es útil a menos que ya le esté dando un número entero. Esperaría que se truncara hacia cero.

Creo que en la mayoría de los casos el que escribe x as u32 realmente quiere x.round() as u32 .

Supongo que depende del dominio, pero espero que x.trunc() as u32 también se desee con bastante frecuencia.

Estoy de acuerdo, pero solo si la pérdida es fácilmente predecible.

Definitivamente estoy de acuerdo. Si 1.6 as u32 convierte en 1 o 2 no debe estar indefinido, por ejemplo.

https://doc.rust-lang.org/nightly/reference/expressions/operator-expr.html#type -cast-expression

La conversión de un flotante a un número entero redondeará el flotante hacia cero.
NOTA: actualmente, esto causará un comportamiento indefinido si el valor redondeado no puede ser representado por el tipo de entero de destino. Esto incluye Inf y NaN. Esto es un error y se solucionará.

La nota se vincula aquí.

El redondeo de valores que “encajan” está bien definido, no es de eso de lo que se trata este tema. Este hilo ya es largo, sería bueno no llevarlo a especulaciones tangentes sobre hechos que ya están establecidos y documentados. Gracias.

Lo que queda por decidir es cómo definir f as $Int en los siguientes casos:

  • f.trunc() > $Int::MAX (incluido infinito positivo)
  • f.trunc() < $Int::MIN (incluido el infinito negativo)
  • f.is_nan()

Una opción que ya está implementada y disponible en Nightly con el indicador del compilador -Z saturating-casts es definirlos para que regresen respectivamente: $Int::MAX , $Int::MIN y cero. Pero aún es posible elegir algún otro comportamiento.

Mi opinión es que el comportamiento definitivamente debería ser determinista y devolver algún valor entero (en lugar de pánico, por ejemplo), pero el valor exacto no es demasiado importante y los usuarios que se preocupan por estos casos deberían utilizar métodos de conversión que propongo por separado. añadir: https://internals.rust-lang.org/t/pre-rfc-add-explicitly-named-numeric-conversion-apis/11395

Supongo que depende del dominio, pero espero que x.trunc() as u32 también se desee con bastante frecuencia.

Correcto. En general, x.anything() as u32 , muy probablemente round() , pero también podría ser trunc() , floor() , ceil() . Solo x as u32 sin especificar el procedimiento de redondeo concreto es probablemente un error.

Mi opinión es que el comportamiento definitivamente debería ser determinista y devolver algún valor entero (en lugar de pánico, por ejemplo), pero el valor exacto no es demasiado importante

Personalmente, estoy bien incluso con el valor "indefinido" siempre que no dependa de nada más que flotar y, lo que es más importante, no exponga ningún registro y contenido de memoria no relacionado.

Una opción que ya está implementada y disponible en Nightly con el indicador del compilador -Z saturating-casts es definirlos para que regresen respectivamente: $Int::MAX , $Int::MIN y cero. Pero aún es posible elegir algún otro comportamiento.

El comportamiento que esperaría obtener para f.trunc() > $Int::MAX y f.trunc() < $Int::MIN es el mismo que cuando el número de punto flotante imaginario se convierte en un número entero de tamaño infinito y luego se devuelven los bits significativos más bajos ( como en la conversión de tipos enteros). Técnicamente, esto sería algunos bits del significativo desplazados hacia la izquierda según el exponente (para números positivos, los números negativos necesitan inversión de acuerdo con el complemento a dos).

Entonces, por ejemplo, esperaría que números realmente grandes se conviertan en 0 .

Parece ser más difícil / más arbitrario definir a qué se convierte el infinito y NaN.

@CryZe, así que si leo eso correctamente, ¿eso coincide con -Z saturating-casts (y lo que Miri ya implementa)?

@RalfJung Eso es correcto.

Genial, copiaré https://github.com/WebAssembly/testsuite/blob/master/conversions.wast (con las trampas reemplazadas por los resultados especificados) al conjunto de pruebas de Miri entonces. :)

@RalfJung Actualice a la última versión de

@sunfishcode ¡ gracias por actualizar! De todos modos tengo que traducir las pruebas a Rust, así que todavía tengo que reemplazar muchas cosas. ;)

¿Las pruebas _sat diferentes en términos de los valores que se están probando? (EDITAR: hay un comentario que dice que los valores son los mismos). Para los elencos saturados de Rust, tomé muchos de estos valores y los agregué en https://github.com/rust-lang/miri/pull/1321. Fui demasiado vago para hacerlo por todos ellos ... pero creo que esto significa que no hay nada que cambiar ahora mismo con el archivo actualizado.

Para el UB intrínseco, las trampas en el lado de wasm deberían convertirse en pruebas de compilación de fallas en Miri, creo.

Los valores de entrada son todos iguales, la única diferencia es que los operadores _sat tienen valores de salida esperados en las entradas donde los operadores de captura tienen trampas esperadas.

Las pruebas para Miri (y por lo tanto también el motor Rust CTFE) se agregaron en https://github.com/rust-lang/miri/pull/1321. Verifiqué localmente que rustc -Zmir-opt-level=0 -Zsaturating-float-casts también pasa las pruebas en ese archivo.
Ahora también implementé el intrínseco no verificado en Miri, consulte https://github.com/rust-lang/miri/pull/1325.

Publiqué https://github.com/rust-lang/rust/pull/71269#issuecomment -615537137 que documenta el estado actual tal como lo entendí y que PR también se mueve para estabilizar el comportamiento de la bandera -Z de saturación.

Dada la longitud de este hilo, creo que si la gente siente que me he perdido algo en ese comentario, diría el comentario al PR o, si es menor, no dude en enviarme un ping en Zulip o Discord (simulacrum) y puedo arregle las cosas para evitar ruidos innecesarios en el hilo de relaciones públicas.

Espero que alguien del equipo de idiomas comience pronto una propuesta de FCP sobre ese RP, y la fusión cerrará automáticamente este problema :)

¿Hay planes para conversiones comprobadas? ¿Algo como fn i32::checked_from(f64) -> Result<i32, DoesntFit> ?

Deberá considerar qué debería devolver i32::checked_from(4.5) .

¿Fue útil esta página
0 / 5 - 0 calificaciones