¿Es la variable local no inicializada el generador de números aleatorios más rápido?

329

Sé que la variable local no inicializada es comportamiento indefinido ( UB ), y también el valor puede tener representaciones de trampa que pueden afectar la operación adicional, pero a veces quiero usar el número aleatorio solo para representación visual y no lo usaré más en otra parte de programa, por ejemplo, establece algo con color aleatorio en un efecto visual, por ejemplo:

void updateEffect(){
    for(int i=0;i<1000;i++){
        int r;
        int g;
        int b;
        star[i].setColor(r%255,g%255,b%255);
        bool isVisible;
        star[i].setVisible(isVisible);
    }
}

¿Es eso más rápido que

void updateEffect(){
    for(int i=0;i<1000;i++){
        star[i].setColor(rand()%255,rand()%255,rand()%255);
        star[i].setVisible(rand()%2==0?true:false);
    }
}

y también más rápido que otro generador de números aleatorios?

ggrr
fuente
88
+1 Esta es una pregunta perfectamente legítima. Es cierto que en la práctica, los valores no inicializados pueden ser algo aleatorios. El hecho de que no lo sean particularmente y de que sea UB no hace que preguntar sea tan malo.
imallett
35
@imallett: Absolutamente. Esta es una buena pregunta, y al menos un viejo juego Z80 (Amstrad / ZX Spectrum) en el pasado usaba su programa como datos para establecer su terreno. Entonces hay incluso precedentes. No puedo hacer eso en estos días. Los sistemas operativos modernos le quitan toda la diversión.
Betsabé
81
Seguramente el principal problema es que no es aleatorio.
juan
30
De hecho, hay un ejemplo de una variable no inicializada que se utiliza como un valor aleatorio, vea el desastre de Debian RNG (Ejemplo 4 en este artículo ).
PaperBirdMaster
31
En la práctica, y créanme, realizo muchas depuraciones en varias arquitecturas; su solución puede hacer dos cosas: leer registros no inicializados o memoria no inicializada. Ahora, mientras "no inicializado" significa aleatorio de cierta manera, en la práctica probablemente contendrá a) ceros , b) valores repetidos o consistentes (en el caso de memoria de lectura anteriormente ocupada por medios digitales) o c) basura consistente con un valor limitado conjunto (en caso de memoria de lectura anteriormente ocupada por datos digitales codificados). Ninguno de esos son fuentes de entropía reales.
mg30rg

Respuestas:

299

Como otros han señalado, este es un comportamiento indefinido (UB).

En la práctica, (probablemente) realmente (más o menos) funcionará. Leer desde un registro no inicializado en arquitecturas x86 [-64] producirá resultados basura, y probablemente no hará nada malo (a diferencia de, por ejemplo, Itanium, donde los registros se pueden marcar como no válidos , de modo que se lee errores de propagación como NaN).

Sin embargo, hay dos problemas principales:

  1. No será particularmente al azar. En este caso, está leyendo desde la pila, por lo que obtendrá lo que estaba allí anteriormente. Lo que podría ser efectivamente aleatorio, completamente estructurado, la contraseña que ingresó hace diez minutos o la receta de galletas de su abuela.

  2. Es una mala práctica (mayúscula 'B') dejar que cosas como estas se cuelen en tu código. Técnicamente, el compilador podría insertar reformat_hdd();cada vez que lea una variable indefinida. No lo hará , pero no debes hacerlo de todos modos. No hagas cosas inseguras. Cuantas menos excepciones haga, más seguro estará de todos los errores accidentales. el tiempo.

El problema más urgente con UB es que hace que el comportamiento de todo su programa sea indefinido. Los compiladores modernos pueden usar esto para evitar grandes extensiones de su código o incluso retroceder en el tiempo . Jugar con UB es como un ingeniero victoriano que desmantela un reactor nuclear en vivo. Hay un montón de cosas que salen mal, y probablemente no conozca la mitad de los principios subyacentes o la tecnología implementada. Se podría estar bien, pero aún así no debería dejar que suceda. Mira las otras buenas respuestas para más detalles.

Además, te despediría.

imallett
fuente
39
@Potatoswatter: los registros de Itanium pueden contener NaT (no es una cosa) que, en efecto, es un "registro no inicializado". En Itanium, leer desde un registro cuando no ha escrito puede anular su programa (lea más sobre esto aquí: blogs.msdn.com/b/oldnewthing/archive/2004/01/19/60162.aspx ). Entonces, hay una buena razón por la cual leer valores no inicializados es un comportamiento indefinido. También es probablemente una de las razones por las que Itanium no es muy popular :)
tbleher
58
Realmente me opongo a la noción de "funciona". Incluso si fuera cierto hoy, que no lo es, podría cambiar en cualquier momento debido a compiladores más agresivos. El compilador puede reemplazar cualquier lectura con unreachable()y eliminar la mitad de su programa. Esto también sucede en la práctica. Este comportamiento neutralizó completamente el RNG en alguna distribución de Linux, creo; La mayoría de las respuestas en esta pregunta parecen suponer que un valor no inicializado se comporta como un valor en absoluto. Eso es falso
usr
25
Además, lo despediría parece algo tonto de decir, suponiendo que las buenas prácticas esto se capte en la revisión del código, se discuta y nunca vuelva a suceder. Esto definitivamente debería entenderse ya que estamos utilizando los indicadores de advertencia correctos, ¿verdad?
Shafik Yaghmour
17
@Michael En realidad, lo es. Si un programa tiene un comportamiento indefinido en algún momento, el compilador puede optimizar su programa de una manera que afecte el código anterior al que invoca el comportamiento indefinido. Hay varios artículos y demostraciones de cómo esto puede ser alucinante. Esto es bastante bueno: blogs.msdn.com/b/oldnewthing/archive/2014/06/27/10537746.aspx (que incluye el bit en el estándar que dice todas las apuestas están canceladas si alguna ruta en su programa invoca UB)
Tom Tanner
19
Esta respuesta suena como si "invocar un comportamiento indefinido es malo en teoría, pero en realidad no te hará mucho daño" . Eso está mal. La recopilación de entropía de una expresión que causaría UB puede (y probablemente lo hará ) hacer que se pierda toda la entropía recopilada anteriormente . Este es un peligro grave.
Theodoros Chatzigiannakis
213

Permítanme decir esto claramente: no invocamos comportamientos indefinidos en nuestros programas . Nunca es una buena idea, punto. Hay raras excepciones a esta regla; por ejemplo, si es un implementador de bibliotecas que implementa offsetof . Si su caso se encuentra bajo tal excepción, probablemente ya lo sepa. En este caso, sabemos que el uso de variables automáticas no inicializadas es un comportamiento indefinido .

Los compiladores se han vuelto muy agresivos con optimizaciones en torno al comportamiento indefinido y podemos encontrar muchos casos en los que el comportamiento indefinido ha dado lugar a fallas de seguridad. ¿El caso más infame es probablemente la eliminación de la comprobación de puntero nulo del kernel de Linux que menciono en mi respuesta al error de compilación de C ++? donde una optimización del compilador en torno al comportamiento indefinido convirtió un bucle finito en uno infinito.

Podemos leer las optimizaciones peligrosas y la pérdida de causalidad de CERT ( video ) que dice, entre otras cosas:

Cada vez más, los escritores de compiladores aprovechan comportamientos indefinidos en los lenguajes de programación C y C ++ para mejorar las optimizaciones.

Con frecuencia, estas optimizaciones interfieren con la capacidad de los desarrolladores para realizar análisis de causa y efecto en su código fuente, es decir, analizar la dependencia de los resultados posteriores de los resultados anteriores.

En consecuencia, estas optimizaciones eliminan la causalidad en el software y aumentan la probabilidad de fallas, defectos y vulnerabilidades del software.

Específicamente con respecto a los valores indeterminados, el informe de defectos estándar C 451: La inestabilidad de las variables automáticas no inicializadas hace una lectura interesante. Todavía no se ha resuelto, pero introduce el concepto de valores tambaleantes, lo que significa que la indeterminación de un valor puede propagarse a través del programa y puede tener diferentes valores indeterminados en diferentes puntos del programa.

No conozco ningún ejemplo de dónde sucede esto, pero en este momento no podemos descartarlo.

Ejemplos reales, no el resultado que esperas

Es poco probable que obtenga valores aleatorios. Un compilador podría optimizar por completo el ciclo. Por ejemplo, con este caso simplificado:

void updateEffect(int  arr[20]){
    for(int i=0;i<20;i++){
        int r ;    
        arr[i] = r ;
    }
}

clang lo optimiza ( verlo en vivo )

updateEffect(int*):                     # @updateEffect(int*)
    retq

o tal vez obtenga todos los ceros, como con este caso modificado:

void updateEffect(int  arr[20]){
    for(int i=0;i<20;i++){
        int r ;    
        arr[i] = r%255 ;
    }
}

verlo en vivo :

updateEffect(int*):                     # @updateEffect(int*)
    xorps   %xmm0, %xmm0
    movups  %xmm0, 64(%rdi)
    movups  %xmm0, 48(%rdi)
    movups  %xmm0, 32(%rdi)
    movups  %xmm0, 16(%rdi)
    movups  %xmm0, (%rdi)
    retq

Ambos casos son formas perfectamente aceptables de comportamiento indefinido.

Tenga en cuenta que si estamos en un Itanium podríamos terminar con un valor de trampa :

[...] si el registro tiene un valor especial que no es nada, lea las trampas del registro, excepto por algunas instrucciones [...]

Otras notas importantes

Es interesante observar la variación entre gcc y clang notada en el proyecto de UB Canarias sobre cuán dispuestos están a aprovechar el comportamiento indefinido con respecto a la memoria no inicializada. Las notas del artículo ( énfasis mío ):

Por supuesto, debemos ser completamente claros con nosotros mismos de que cualquier expectativa no tiene nada que ver con el estándar del lenguaje y todo lo que tiene que ver con lo que un compilador en particular hace, ya sea porque los proveedores de ese compilador no están dispuestos a explotar esa UB o simplemente porque aún no han llegado a explotarlo . Cuando no existe una garantía real del proveedor del compilador, nos gusta decir que los UB aún no explotados son bombas de tiempo : están esperando para explotar el próximo mes o el próximo año cuando el compilador se vuelva un poco más agresivo.

Como Matthieu M. señala, lo que todo programador C debe saber sobre el comportamiento indefinido # 2/3 también es relevante para esta pregunta. Dice entre otras cosas ( énfasis mío ):

Lo importante y aterrador es darse cuenta de que casi cualquier optimización basada en un comportamiento indefinido puede comenzar a activarse en un código defectuoso en cualquier momento en el futuro . La alineación, el desenrollado de bucles, la promoción de memoria y otras optimizaciones seguirán mejorando, y una parte importante de su razón para existir es exponer optimizaciones secundarias como las anteriores.

Para mí, esto es profundamente insatisfactorio, en parte porque el compilador inevitablemente termina siendo culpado, pero también porque significa que enormes cuerpos de código C son minas terrestres que esperan explotar.

Para completar, probablemente debería mencionar que las implementaciones pueden optar por hacer que el comportamiento indefinido esté bien definido, por ejemplo, gcc permite la escritura de tipos a través de uniones, mientras que en C ++ esto parece un comportamiento indefinido . Si este es el caso, la implementación debería documentarlo y, por lo general, esto no será portátil.

Shafik Yaghmour
fuente
1
+ (int) (PI / 3) para los ejemplos de salida del compilador; Un ejemplo de la vida real de que UB es, bueno, UB .
2
Utilizar UB efectivamente solía ser la marca registrada de un excelente hacker. Esta tradición ha continuado durante probablemente 50 años o más ahora. Desafortunadamente, ahora se requieren computadoras para minimizar los efectos de UB debido a las malas personas. Realmente disfruté descubrir cómo hacer cosas geniales con el código de máquina UB o la lectura / escritura de puertos, etc. En los años 90, cuando el sistema operativo no era tan capaz de proteger al usuario de sí mismos.
sfdcfox
1
@sfdcfox si lo estaba haciendo en código / ensamblador de máquina, no era un comportamiento indefinido (puede haber sido un comportamiento no convencional).
Caleth
2
Si tiene en mente un ensamblaje específico, úselo y no escriba C. no compatible. Entonces todos sabrán que está utilizando un truco no portátil específico. Y no es Bad People lo que significa que no puedes usar UB, es Intel, etc., haciendo sus trucos en el chip.
Caleth
2
@ 500-InternalServerError porque pueden no ser fácilmente detectables o pueden no ser detectables en absoluto en el caso general y, por lo tanto, no habría forma de rechazarlos. Lo cual es diferente a las violaciones de la gramática que se pueden detectar. También tenemos diagnósticos mal formados y mal formados que, en general, separan los programas mal formados que podrían detectarse en teoría de aquellos que en teoría no podrían detectarse de manera confiable.
Shafik Yaghmour
164

No, es terrible

El comportamiento de usar una variable no inicializada no está definido tanto en C como en C ++, y es muy poco probable que dicho esquema tenga propiedades estadísticas deseables.

Si quieres un generador de números aleatorios "rápido y sucio", entonces rand()es tu mejor opción. En su implementación, todo lo que hace es una multiplicación, una suma y un módulo.

El generador más rápido que conozco requiere que utilices a uint32_tcomo tipo de variable pseudoaleatoria I, y utilices

I = 1664525 * I + 1013904223

para generar valores sucesivos. Puede elegir cualquier valor inicial de I(llamado semilla ) que desee. Obviamente puedes codificar eso en línea. La envoltura garantizada estándar de un tipo sin signo actúa como módulo. (Las constantes numéricas son cuidadosamente seleccionadas por el notable programador científico Donald Knuth).

Betsabé
fuente
99
El generador "congruencial lineal" que presenta es bueno para aplicaciones simples, pero solo para aplicaciones no criptográficas. Es posible predecir su comportamiento. Véase, por ejemplo, " Descifrando un cifrado lineal congruencial " por el propio Don Knuth (IEEE Transactions on Information Theory, Volumen 31)
Jay
24
@Jay en comparación con una variable unitaria para rápido y sucio? Esta es una solución mucho mejor.
Mike McMahon
2
rand()no es adecuado para su propósito y, en mi opinión, debería ser totalmente obsoleto. En estos días, puede descargar generadores de números aleatorios con licencia superior e inmensamente superiores (por ejemplo, Mersenne Twister) que son casi tan rápidos con la mayor facilidad, por lo que realmente no hay necesidad de continuar usando el altamente defectuosorand()
Jack Aidley
1
rand () tiene otro problema terrible: utiliza un tipo de bloqueo, llamado subprocesos internos, ralentiza drásticamente su código. Al menos, hay una versión reentrante. Y si usa C ++ 11, la API aleatoria proporciona todo lo que necesita.
Marwan Burelle
44
Para ser justos, no preguntó si era un buen generador de números aleatorios. Preguntó si fue rápido. Bueno, sí, probablemente sea el ayuno. Pero los resultados no serán muy aleatorios en absoluto.
jcoder
42

¡Buena pregunta!

Indefinido no significa que sea aleatorio. Piénselo, los valores que obtendría en variables globales no inicializadas fueron dejados allí por el sistema o su / otras aplicaciones en ejecución. Dependiendo de lo que haga su sistema con la memoria que ya no se usa y / o qué tipo de valores genera el sistema y las aplicaciones, puede obtener:

  1. Siempre lo mismo.
  2. Sé uno de un pequeño conjunto de valores.
  3. Obtenga valores en uno o más rangos pequeños.
  4. Vea muchos valores divisibles por 2/4/8 de punteros en un sistema de 16/32/64 bits
  5. ...

Los valores que obtendrá dependen completamente de los valores no aleatorios que deja el sistema y / o las aplicaciones. Por lo tanto, de hecho habrá algo de ruido (a menos que los borrados de su sistema ya no usen memoria), pero el conjunto de valores del que extraerá de ninguna manera será aleatorio.

Las cosas empeoran mucho para las variables locales porque provienen directamente de la pila de su propio programa. Existe una muy buena posibilidad de que su programa realmente escriba estas ubicaciones de pila durante la ejecución de otro código. Calculo que las posibilidades de suerte en esta situación son muy bajas, y un cambio de código 'aleatorio' que realice prueba esta suerte.

Leer sobre aleatoriedad . Como verá, la aleatoriedad es una propiedad muy específica y difícil de obtener. Es un error común pensar que si solo toma algo que es difícil de rastrear (como su sugerencia) obtendrá un valor aleatorio.

el significado importa
fuente
77
... y eso está dejando de lado todas las optimizaciones del compilador que destriparían por completo ese código.
Deduplicador el
6 ... Obtendrá diferentes "aleatorias" en Debug and Release. Indefinido significa que lo estás haciendo mal.
Sql Surfer
Correcto. Abreviaría o resumiría con "undefined"! = "Arbitrary"! = "Random". Todos estos tipos de "desconocimiento" tienen diferentes propiedades.
fche
Se garantiza que las variables globales tienen un valor definido, ya sea que se inicialicen explícitamente o no. Esto es definitivamente cierto en C ++ y en C también .
Brian Vandenberg
32

Muchas buenas respuestas, pero me permiten agregar otra y enfatizar el punto de que en una computadora determinista, nada es aleatorio. Esto es cierto tanto para los números producidos por un pseudo-RNG como para los números aparentemente "aleatorios" que se encuentran en áreas de memoria reservadas para variables locales C / C ++ en la pila.

PERO ... hay una diferencia crucial.

Los números generados por un buen generador pseudoaleatorio tienen las propiedades que los hacen estadísticamente similares a los sorteos verdaderamente aleatorios. Por ejemplo, la distribución es uniforme. La duración del ciclo es larga: puede obtener millones de números aleatorios antes de que el ciclo se repita. La secuencia no está autocorrelacionada: por ejemplo, no comenzará a ver emerger patrones extraños si toma cada segundo, tercero o 27º número, o si observa dígitos específicos en los números generados.

Por el contrario, los números "aleatorios" que quedan en la pila no tienen ninguna de estas propiedades. Sus valores y su aparente aleatoriedad dependen completamente de cómo se construye el programa, cómo se compila y cómo el compilador lo optimiza. A modo de ejemplo, aquí hay una variación de su idea como programa autónomo:

#include <stdio.h>

notrandom()
{
        int r, g, b;

        printf("R=%d, G=%d, B=%d", r&255, g&255, b&255);
}

int main(int argc, char *argv[])
{
        int i;
        for (i = 0; i < 10; i++)
        {
                notrandom();
                printf("\n");
        }

        return 0;
}

Cuando compilo este código con GCC en una máquina Linux y lo ejecuto, resulta ser bastante desagradable determinista:

R=0, G=19, B=0
R=130, G=16, B=255
R=130, G=16, B=255
R=130, G=16, B=255
R=130, G=16, B=255
R=130, G=16, B=255
R=130, G=16, B=255
R=130, G=16, B=255
R=130, G=16, B=255
R=130, G=16, B=255

Si miraras el código compilado con un desensamblador, podrías reconstruir lo que estaba sucediendo, en detalle. La primera llamada a notrandom () usó un área de la pila que este programa no usó anteriormente; Quién sabe lo que había allí. Pero después de esa llamada a notrandom (), hay una llamada a printf () (que el compilador GCC realmente optimiza para una llamada a putchar (), pero no importa) y que sobrescribe la pila. Por lo tanto, la próxima y las siguientes veces, cuando se llame a notrandom (), la pila contendrá datos obsoletos de la ejecución de putchar (), y dado que putchar () siempre se llama con los mismos argumentos, estos datos obsoletos siempre serán los mismos, también.

Por lo tanto, no hay absolutamente nada al azar sobre este comportamiento, ni los números obtenidos de esta manera tienen ninguna de las propiedades deseables de un generador de números pseudoaleatorios bien escrito. De hecho, en la mayoría de los escenarios de la vida real, sus valores serán repetitivos y altamente correlacionados.

De hecho, como otros, también consideraría seriamente despedir a alguien que intentó pasar esta idea como un "RNG de alto rendimiento".

Viktor Toth
fuente
1
"En una computadora determinista, nada es aleatorio" - Esto no es realmente cierto. Las computadoras modernas contienen todo tipo de sensores que le permiten producir aleatorias verdaderas e impredecibles sin generadores de hardware separados. En una arquitectura moderna, los valores de a /dev/randommenudo se derivan de tales fuentes de hardware, y de hecho son "ruido cuántico", es decir, verdaderamente impredecibles en el mejor sentido físico de la palabra.
Konrad Rudolph
2
Pero entonces, esa no es una computadora determinista, ¿verdad? Ahora confía en el aporte ambiental. En cualquier caso, esto nos lleva más allá de la discusión de un pseudo-RNG convencional frente a bits "aleatorios" en la memoria no inicializada. Además ... mire la descripción de / dev / random para apreciar cuán lejos de su camino los implementadores se aseguraron de que los números aleatorios sean criptográficamente seguros ... precisamente porque las fuentes de entrada no son ruido cuántico puro, no correlacionado, sino más bien, lecturas de sensores potencialmente altamente correlacionadas con solo un pequeño grado de aleatoriedad. También es bastante lento.
Viktor Toth
29

El comportamiento indefinido significa que los autores de compiladores son libres de ignorar el problema porque los programadores nunca tendrán derecho a quejarse de lo que suceda.

Si bien, en teoría, al ingresar a la tierra de UB, cualquier cosa puede suceder (incluido un demonio volando por la nariz ), lo que normalmente significa es que a los autores del compilador simplemente no les importará y, para las variables locales, el valor será lo que esté en la memoria de la pila en ese punto .

Esto también significa que a menudo el contenido será "extraño" pero fijo o ligeramente aleatorio o variable, pero con un patrón evidente claro (por ejemplo, valores crecientes en cada iteración).

Seguro que no puedes esperar que sea un generador aleatorio decente.

6502
fuente
28

El comportamiento indefinido es indefinido. No significa que obtenga un valor indefinido, significa que el programa puede hacer cualquier cosa y aún así cumplir con las especificaciones del lenguaje.

Un buen compilador de optimización debería tomar

void updateEffect(){
    for(int i=0;i<1000;i++){
        int r;
        int g;
        int b;
        star[i].setColor(r%255,g%255,b%255);
        bool isVisible;
        star[i].setVisible(isVisible);
    }
}

y compilarlo en un noop. Esto es ciertamente más rápido que cualquier otra alternativa. Tiene la desventaja de que no hará nada, pero esa es la desventaja del comportamiento indefinido.

Martijn
fuente
3
Mucho depende de si el propósito de un compilador es ayudar a los programadores a producir archivos ejecutables que cumplan con los requisitos del dominio, o si el propósito es producir el ejecutable más "eficiente" cuyo comportamiento será consistente con los requisitos mínimos del Estándar C, sin considerar si tal comportamiento servirá para algún propósito útil. Con respecto al objetivo anterior, hacer que el código use algunos valores iniciales arbitrarios para r, g, b, o desencadenar una trampa de depuración si fuera práctico, sería más útil que convertir el código en un nop. Con respecto a este último objetivo ...
supercat
2
... un compilador óptimo debería determinar qué entradas causarían la ejecución del método anterior y eliminar cualquier código que solo sería relevante cuando se reciban tales entradas.
supercat
1
@supercat O su propósito podría ser C. producir archivos ejecutables eficientes que cumplan con el Estándar mientras ayudan al programador a encontrar lugares donde el cumplimiento puede no ser útil. Los compiladores pueden cumplir con este propósito de compromiso emitiendo más diagnósticos de los que requiere el Estándar, como los GCC -Wall -Wextra.
Damian Yerrick
1
Que los valores estén indefinidos no significa que el comportamiento del código circundante esté indefinido. Ningún compilador debe noop esa función. Las dos llamadas de función, cualesquiera que sean las entradas que se den, DEBEN llamarse absolutamente; el primero DEBE llamarse con tres números entre 0 y 255, y el segundo DEBE llamarse con un valor verdadero o falso. Un "buen compilador de optimización" podría optimizar los parámetros de la función a valores estáticos arbitrarios, deshaciéndose por completo de las variables, pero eso es todo lo que podría llegar (bueno, a menos que las funciones mismas se puedan reducir a noops en ciertas entradas).
Dewi Morgan el
@DewiMorgan: como las funciones llamadas son del tipo "establecer este parámetro", casi con certeza se reducen a noops cuando la entrada es igual al valor actual del parámetro, que el compilador puede asumir que es el caso.
Julio
18

Todavía no se menciona, pero las rutas de código que invocan un comportamiento indefinido pueden hacer lo que el compilador quiera, p. Ej.

void updateEffect(){}

Lo cual es ciertamente más rápido que su bucle correcto, y debido a UB, es perfectamente conforme.

Caleth
fuente
18

Por razones de seguridad, se debe limpiar la nueva memoria asignada a un programa; de lo contrario, se podría usar la información y las contraseñas podrían filtrarse de una aplicación a otra. Solo cuando reutiliza la memoria, obtiene valores diferentes a 0. Y es muy probable que en una pila el valor anterior sea solo fijo, porque el uso anterior de esa memoria es fijo.

Arne
fuente
13

Su ejemplo de código particular probablemente no haría lo que espera. Si bien técnicamente cada iteración del bucle recrea las variables locales para los valores r, g y b, en la práctica es exactamente el mismo espacio de memoria en la pila. Por lo tanto, no se volverá a aleatorizar con cada iteración, y terminará asignando los mismos 3 valores para cada uno de los 1000 colores, independientemente de cuán aleatorios sean r, g y b individualmente e inicialmente.

De hecho, si funcionara, tendría mucha curiosidad sobre lo que lo aleatoriza. Lo único que se me ocurre es una interrupción intercalada que se coloca encima de esa pila, muy poco probable. Quizás la optimización interna que los mantuvo como variables de registro en lugar de como ubicaciones de memoria real, donde los registros se reutilizan más abajo en el bucle, también sería el truco, especialmente si la función de visibilidad establecida es particularmente hambrienta de registros. Aún así, lejos de ser al azar.

Jos
fuente
12

Como la mayoría de las personas aquí mencionó el comportamiento indefinido. Indefinido también significa que puede obtener algún valor entero válido (por suerte) y en este caso será más rápido (ya que no se realiza la llamada a la función rand). Pero prácticamente no lo uses. Estoy seguro de que esto tendrá resultados terribles ya que la suerte no está contigo todo el tiempo.

Ali Kazmi
fuente
1
Muy buen punto! Puede ser un truco pragmático, pero de hecho uno que requiere suerte.
significado-asuntos
1
No hay absolutamente ninguna suerte involucrada. Si el compilador no optimiza el comportamiento indefinido, los valores que obtenga serán perfectamente deterministas (= dependerán completamente de su programa, sus entradas, su compilador, las bibliotecas que usa, el tiempo de sus subprocesos si tiene subprocesos). El problema es que no puedes razonar sobre estos valores, ya que dependen de los detalles de implementación.
cmaster - reinstalar a monica el
En ausencia de un sistema operativo con una pila de manejo de interrupciones separada de la pila de aplicaciones, la suerte puede estar involucrada, ya que las interrupciones frecuentemente perturbarán el contenido de la memoria un poco más allá de los contenidos actuales de la pila.
supercat
12

¡Muy mal! Mal hábito, mal resultado. Considerar:

A_Function_that_use_a_lot_the_Stack();
updateEffect();

Si la función A_Function_that_use_a_lot_the_Stack()realiza siempre la misma inicialización, deja la pila con los mismos datos. Esos datos son lo que llamamos updateEffect(): ¡ siempre el mismo valor! .

Frankie_C
fuente
11

Realicé una prueba muy simple, y no fue aleatoria en absoluto.

#include <stdio.h>

int main() {

    int a;
    printf("%d\n", a);
    return 0;
}

Cada vez que ejecuté el programa, imprimió el mismo número ( 32767en mi caso), no puede ser mucho menos aleatorio que eso. Esto es presumiblemente cualquiera que sea el código de inicio en la biblioteca de tiempo de ejecución dejado en la pila. Como usa el mismo código de inicio cada vez que se ejecuta el programa, y ​​nada más varía en el programa entre ejecuciones, los resultados son perfectamente consistentes.

Barmar
fuente
Buen punto. Un resultado depende en gran medida de dónde se llama este generador de números "aleatorios" en el código. Es bastante impredecible que al azar.
NO_NAME
10

Debe tener una definición de lo que quiere decir con "aleatorio". Una definición sensata implica que los valores que obtienes deben tener poca correlación. Eso es algo que puedes medir. Tampoco es trivial lograrlo de manera controlada y reproducible. Así que el comportamiento indefinido ciertamente no es lo que estás buscando.

Zsolt Szatmari
fuente
7

Hay ciertas situaciones en las que la memoria no inicializada se puede leer de forma segura utilizando el tipo "unsigned char *" [por ejemplo, un búfer devuelto desde malloc]. El código puede leer dicha memoria sin tener que preocuparse de que el compilador arroje la causalidad por la ventana, y hay momentos en que puede ser más eficiente tener el código preparado para cualquier cosa que pueda contener la memoria que para garantizar que no se leerán los datos no inicializados ( Un ejemplo común de esto sería usar memcpyun búfer parcialmente inicializado en lugar de copiar discretamente todos los elementos que contienen datos significativos).

Sin embargo, incluso en tales casos, siempre se debe suponer que si alguna combinación de bytes será particularmente irritante, leerla siempre generará ese patrón de bytes (y si un cierto patrón sería irritante en la producción, pero no en el desarrollo, tal patrón no aparecerá hasta que el código esté en producción).

La lectura de memoria no inicializada podría ser útil como parte de una estrategia de generación aleatoria en un sistema embebido donde uno puede estar seguro de que la memoria nunca se ha escrito con contenido sustancialmente no aleatorio desde la última vez que se encendió el sistema, y ​​si la fabricación El proceso utilizado para la memoria hace que su estado de encendido varíe de forma semialeatoria. El código debería funcionar incluso si todos los dispositivos siempre producen los mismos datos, pero en casos donde, por ejemplo, un grupo de nodos necesita seleccionar ID únicos arbitrarios lo más rápido posible, tener un generador "no muy aleatorio" que le da a la mitad de los nodos la misma inicial La identificación podría ser mejor que no tener ninguna fuente inicial de aleatoriedad en absoluto.

Super gato
fuente
2
"si alguna combinación de bytes será particularmente irritante, leerla siempre producirá ese patrón de bytes", hasta que codifique para hacer frente a ese patrón, en cuyo punto ya no será irritante y se leerá un patrón diferente en el futuro.
Steve Jessop
@SteveJessop: Precisamente. Mi línea sobre desarrollo vs producción tenía la intención de transmitir una noción similar. El código no debería preocuparse por lo que hay en la memoria no inicializada más allá de una vaga noción de "Alguna aleatoriedad podría ser agradable". Si el comportamiento del programa se ve afectado por el contenido de una pieza de memoria no inicializada, el contenido de las piezas que se adquieren en el futuro puede verse afectado por eso.
supercat
5

Como otros han dicho, será rápido, pero no al azar.

Lo que la mayoría de los compiladores harán para las variables locales es obtener algo de espacio en la pila, pero no molestarse en configurarlo (el estándar dice que no es necesario, entonces, ¿por qué ralentizar el código que está generando?).

En este caso, el valor que obtendrá dependerá de lo que estaba previamente en la pila: si llama a una función anterior a esta que tiene un centenar de variables de caracteres locales todas configuradas en 'Q' y luego llama a su función después que regresa, entonces probablemente encontrará que sus valores "aleatorios" se comportan como si los tuviera memset()todos a 'Q'.

Es importante destacar que para su función de ejemplo que intenta usar esto, estos valores no cambiarán cada vez que los lea, serán los mismos cada vez. Por lo tanto, obtendrá un total de 100 estrellas con el mismo color y visibilidad.

Además, nada dice que el compilador no debe inicializar estos valores, por lo que un compilador futuro podría hacerlo.

En general: mala idea, no lo hagas. (como muchas optimizaciones de nivel de código "inteligentes" realmente ...)

Alun Thomas
fuente
2
Estás haciendo algunas predicciones sólidas sobre lo que sucederá, aunque nada de eso está garantizado debido a UB. Tampoco es cierto en la práctica.
usr
3

Como otros ya han mencionado, este es un comportamiento indefinido ( UB ), pero puede "funcionar".

Excepto por los problemas ya mencionados por otros, veo otro problema (desventaja): no funcionará en ningún lenguaje que no sea C y C ++. Sé que esta pregunta es sobre C ++, pero si puede escribir código que será un buen código C ++ y Java y no es un problema, ¿por qué no? Tal vez algún día alguien tenga que portarlo a otro idioma y buscar errores causados ​​por "trucos de magia" UB como este definitivamente será una pesadilla (especialmente para un desarrollador inexperto de C / C ++).

Aquí hay una pregunta sobre otra UB similar. Solo imagínese tratando de encontrar un error como este sin saber acerca de este UB. Si desea leer más sobre cosas tan extrañas en C / C ++, lea las respuestas a las preguntas desde el enlace y vea esta GRAN presentación de diapositivas. Le ayudará a comprender qué hay debajo del capó y cómo está funcionando; no es solo otra presentación de diapositivas llena de "magia". Estoy bastante seguro de que incluso la mayoría de los programadores experimentados de C / c ++ pueden aprender mucho de esto.

cyriel
fuente
3

No es una buena idea confiar nuestra lógica en el comportamiento indefinido del lenguaje. Además de lo mencionado / discutido en esta publicación, me gustaría mencionar que con el enfoque / estilo moderno de C ++, tal programa no puede compilarse.

Esto se mencionó en mi publicación anterior que contiene la ventaja de la función automática y un enlace útil para la misma.

https://stackoverflow.com/a/26170069/2724703

Entonces, si cambiamos el código anterior y reemplazamos los tipos reales con auto , el programa ni siquiera se compilaría.

void updateEffect(){
    for(int i=0;i<1000;i++){
        auto r;
        auto g;
        auto b;
        star[i].setColor(r%255,g%255,b%255);
        auto isVisible;
        star[i].setVisible(isVisible);
    }
}
Mantosh Kumar
fuente
3

Me gusta tu forma de pensar. Realmente fuera de la caja. Sin embargo, la compensación realmente no vale la pena. La compensación del tiempo de ejecución de la memoria es una cosa, incluso el comportamiento indefinido para el tiempo de ejecución no lo es .

Debe darle una sensación muy inquietante saber que está utilizando un método tan "aleatorio" como su lógica de negocios. No lo haré.

DDan
fuente
3

Use 7757todos los lugares donde tenga la tentación de usar variables no inicializadas. Lo elegí al azar de una lista de números primos:

  1. es comportamiento definido

  2. se garantiza que no siempre será 0

  3. es primo

  4. es probable que sea estadísticamente aleatorio como variables no inicializadas

  5. es probable que sea más rápido que las variables no inicializadas ya que su valor se conoce en tiempo de compilación

Glenn Teitelbaum
fuente
Para comparar, vea los resultados en esta respuesta: stackoverflow.com/a/31836461/2963099
Glenn Teitelbaum
1

Hay una posibilidad más para considerar.

Los compiladores modernos (ejem g ++) son tan inteligentes que revisan su código para ver qué instrucciones afectan el estado y qué no, y si se garantiza que una instrucción NO afectará el estado, g ++ simplemente eliminará esa instrucción.

Entonces, esto es lo que sucederá. g ++ definitivamente verá que está leyendo, realizando operaciones aritméticas, guardando, lo que es esencialmente un valor basura, que produce más basura. Como no hay garantía de que la nueva basura sea más útil que la anterior, simplemente eliminará su ciclo. BLOOP!

Este método es útil, pero esto es lo que haría. Combine UB (Comportamiento indefinido) con velocidad rand ().

Por supuesto, reduce los rand()s ejecutados, pero mézclalos para que el compilador no haga nada que no quieras.

Y no te despediré.

ps95
fuente
Me resulta muy difícil creer que un compilador pueda decidir que su código está haciendo algo tonto y eliminarlo. Esperaría que solo optimice el código no utilizado , no el código desaconsejado . ¿Tienes un caso de prueba reproducible? De cualquier manera, la recomendación de UB es peligrosa. Además, GCC no es el único compilador competente, por lo que es injusto señalarlo como "moderno".
underscore_d
-1

El uso de datos no inicializados para aleatoriedad no es necesariamente algo malo si se hace correctamente. De hecho, OpenSSL hace exactamente esto para sembrar su PRNG.

Aparentemente, este uso no estaba bien documentado, porque alguien notó que Valgrind se quejaba de usar datos no inicializados y los "reparó", causando un error en el PRNG .

Para que pueda hacerlo, pero necesita saber lo que está haciendo y asegurarse de que cualquiera que lea su código entienda esto.

dbush
fuente
1
Esto dependerá de su compilador, que se espera con un comportamiento indefinido, como podemos ver en mi respuesta, el sonido de hoy no hará lo que quieran.
Shafik Yaghmour
66
Que OpenSSL haya usado este método como entrada de entropía no dice que haya sido bueno. Después de todo, la única otra fuente de entropía que usaron fue el PID . No es exactamente un buen valor aleatorio. De alguien que confía en una fuente de entropía tan mala, no esperaré un buen juicio sobre su otra fuente de entropía. Solo espero que las personas que actualmente mantienen OpenSSL sean más brillantes.
cmaster - reinstalar a monica el