Título: campos de bits Publicado por: michael_753 en 26 Septiembre 2023, 03:22 am Hola buenas, estoy arrancando con el tema de campos de bits y tengo el siguiente problema que no lo estaría entendiendo del todo:
Realizar un programa que permita almacenar la información de 10 los diez productos que contiene un depósito en stock, por cada uno se debe almacenar: Código (1 a 10) Cantidad que puede variar entre 0 y 500 Si es Nacional o importado Definir la estructura de forma de optimizar lo más posible el uso de la memoria. Espero me puedan ayudar a resolverlo para ver como es que se resuelven gracias. Título: Re: campos de bits Publicado por: MAFUS en 26 Septiembre 2023, 09:49 am Te voy a preguntar ¿Qué sabes de los campos de bits? La pregunta tuya es muy abierta.
En el foro nos gusta ir al detalle de lo que no sabes, la duda concreta. No nos gusta hacer el ejercicio. Título: Re: campos de bits Publicado por: michael_753 en 27 Septiembre 2023, 00:49 am Hola, ahí mande el código con lo que hice hasta ahora ( en comentarios están las dudas)
Código: [code=c]#include <stdio.h> Título: Re: campos de bits Publicado por: MAFUS en 27 Septiembre 2023, 20:30 pm Creo que sé lo que intentas hacer y he modificado un poco tu programa.
Por cierto, de procedencia dijiste que solo era nacional o extranjera, para ello basta con solo 1 bit, no 3. He puesto algunos comentarios explicativos. Código
Lo curioso es que me ha salido un resultado interesante para mostrar que el compilador hace lo que quiere con las estructuras. Adelantándome a tu respuesta sí, hay formas de obligarlo a que haga las estructuras exactamente como le has dicho, pero de normal intenta optimizarlas al máximo. Uno de los resultados que me ha dado ha sido este: producto ---------- codigo: 8 procedencia: 0 cantidad: 68 representacion interna: 0888 Me ha gustado mucho ese 888 hexadecimal pues se puede inferir como están los campos dentro. Veamos: representacion interna: 0 8 8 8 0000 1000 1000 1000 codigo: 8 [ 1000 ] procedencia: 0 [ 0 ] cantidad: 68 [ 001000100 ] -relleno- cantidad procedencia codigo 00 001000100 0 1000 Yo había construido el mapa de bits en el orden codigo -> procedencia -> cantidad y el compilador lo ha construido de forma interna como cantidad -> procedencia -> codigo Título: Re: campos de bits Publicado por: michael_753 en 27 Septiembre 2023, 21:16 pm Muchas gracias por tomarte el tiempo, ya pude comprender un poco mas el tema.
Título: Re: campos de bits Publicado por: michael_753 en 28 Septiembre 2023, 01:44 am Hola, te hago otra consulta.
En código que me pasaste vos lo que hiciste fue como EMPAQUETAR los datos que se van generando correcto ?. Bueno yo lo edite un poco para que me DESEMPAQUETE el numero generado, y el problema es que los valores no verifican. Mando el código y un ejemplo. Código
Por ejemplo un valor que sale es : 254 = 00FE = 0000 0000 1111 1110 (2 byte) entonces, según como declare los campos: código, cantidad, procedencia, basura código seria: 1110 = 14 // son los 4 bits menos significativos cantidad : 0 0000 1111 = debería ser 15 pero sale 254 (igual que valor que arrojo el rand) procedencia: 0 bas: 00 No se si me explique, espero su respuesta y muchas gracias. Título: Re: campos de bits Publicado por: MAFUS en 28 Septiembre 2023, 04:50 am ¿Qué ha pasado?
Le has dado unas directrices a C en tu construcción del mapa de bits y ha intentado acomodarlo como ha podido. ¿Cómo lo ha acomodado? Vamos a reducir el programa a la mínima expresión: Código
Resultado: Código: tamanno del mapa de bits: 6 bytes En vez de Código
ponemos Código
y resulta en Código: tamanno del mapa de bits: 6 bytes Podemos ver que alineó codigo en 2 bytes (aunque solo tuviera 4 bits) y cantidad también la ha alineado en otros dos bytes. Ya te digo también que he estado buscando procedencia y basura y no los he encontrado, pero están ahí. En el mejor de los casos está usando un byte en otro sitio para guardar esa información. Por otra parte ocurre una cosa muy interesante. Si juntamos todos los unsigned char dentro del mapa de bits la cosa cambia: Código
El resultado es el siguiente: Código: tamanno del mapa de bits: 4 bytes Vamos a exprimir un poco más el mapa de bits. ¿Qué pasaría si hubieras usado unsigned short en todos los tipos de datos dentro del mapa? Código
Esto da como resultado: Código: tamanno del mapa de bits: 2 bytes De hecho te recomiendo quitar bas, porque no se usa. En estructuras más complicadas sí que se usan bits para alinear la estructura interna, y solo tienen ese propósito, y estos son anónimos, no tienen nombre. Por ejemplo: Código
Como dato curioso también existen los anónimos de 0 bits. Estos lo que le indican al compilador es que alinee los campos de bits que le siguen a una potencia de dos. De nuevo todo esto es para facilitar al procesador el movimiento de datos en la memoria. Por ejemplo: Código
Me devuelve el siguiente resultado: Código: tamanno del mapa de bits: 4 bytes ¿Qué quiere decir todo esto? No es buena idea usar union de esta forma, es decir, para empaquetar estructuras. Ya has visto que el compilador hace lo que quiere con ellas. Más adelante te encontrarás que incluso guardar una estructura entera en disco o mandarla por la red te dará problemas porque al rescatarla leerás basura. Esto es debido, otra vez, a que el compilador hace lo que quiere con las estructuras y si a ti te las ha colocado de una forma en memoria, a otro puede que tenga sus miembros de forma diferente y por lo tanto se colocarán mal los datos. Pero también puede que en las dos máquinas las estructuras esté construidas igual y consigas bien la información. Esto quiere decir que para serializar y deserializar estructuras manda al stream cada miembro de la estructura, uno a uno, en una secuencia dada. Recupera del stream uno a uno los miembros de la estructura en el mismo orden que los enviaste. Por último, y como ya te habrás dado cuenta, dar diferentes tipos de datos a los miembros de un mapa de bits hace que el compilador intente agruparlos por tipo. Mezclarlos, como has visto, resulta en un desastre. Te recomendaría que uses unsigned y el compilador te lo va a acomodar todo en 32 bits. Si necesitas un control muy fino de bits en memoria deberías pensar en operaciones de desplazamiento de bits junto a operaciones lógicas. Los mapas de bits son más para no gastar mucha memoria en números pequeños y caso es buen ejemplo de ellos. Un dato que perfectamente cabe en 4 bits, otro que cabe en 9 y otro que cabe en 1, puedes meterlo todo en 2 bytes en vez de gastar 12, que sería si los hubieras declarado como tres enteros diferentes. Los campos de bits se ven mucho más en sistemas embebidos y microcontroladores. Título: Re: campos de bits Publicado por: michael_753 en 28 Septiembre 2023, 05:23 am Excelente, ahí hice las correcciones y funciono de maravilla, muchas gracias también por la explicación.
Título: Re: campos de bits Publicado por: RayR en 29 Septiembre 2023, 05:00 am Como ya te recomendaron, es mejor que evites los campos de bits si puedes, ya que no tienes casi ningún control sobre la manera en que el compilador los almacena.
También hay unas cosas que creo que vale la pena comentar. Los compiladores tienen ciertas libertades con las estructuras, pero lo que hacen no es aleatorio, y además hay reglas que tienen que seguir. Si dejamos de lado de momento los campos de bits, tenemos algunas garantías con las estructuras: el compilador siempre colocará en memoria los campos en el orden en que los declaramos. Puede haber relleno (padding) entre algunos, pero el orden siempre se respeta. Y la dirección del primer campo es la misma que la de la estructura, es decir, está exactamente al inicio. Luego, hay otras reglas no oficiales, pero que los compiladores en general siguen. Por ejemplo, los compiladores normalmente acomodan los campos de acuerdo a su alineación natural, es decir, en qué direcciones de memoria conviene que estén. Si para lograrlo deben meter bytes de relleno (padding), lo hacen. Esto en general se puede evitar declarándolos de manera que ya estén alineados (no basta con sólo agruparlos por tipo). No sé si quisieras información más detallada, pero como estás empezando en estos temas, a lo mejor te podría confundir. Con los campos de bits es más complicado y depende del compilador y la arquitectura, pero tampoco es totalmente arbitrario. De nuevo, sin entrar en detalles, baste decir que tiene sentido la manera en que los está organizando y ordenando el compilador en este caso y que, aunque acostumbramos leer de izquierda a derecha, en realidad los dígitos se enumeran de derecha a izquierda. En los últimos ejemplos, el problema principal es, como ya te dijeron, el hecho de usar union. Y esto no sólo aplica a las estructuras, sino que, en general, se supone que sólo uno de sus miembros es válido en un momento dado: el último que hayas modificado. Es decir, si haces esto: Código
lo más correcto sería que, a partir de ahí, consideres a vec.empaquetado como inactivo o "inválido" y no accedas a él. Si después modificaras el otro miembro: Código
ahora este miembro es el activo y vec.producto se inactiva, y así sucesivamente. De hecho, en C++ es incorrecto acceder al miembro "inactivo" (aunque compila y en general funciona, técnicamente no es válido). En C se permite y a veces se hace, pero no se recomienda, pues puede dar pie a errores. Finalmente, para ver la representación interna de una estructura, es mejor usar punteros a unsigned char, no unions, y menos mediante un miembro entero, pues ¿qué tipo de dato debería tener empaquetado, si no sabemos a priori cuál es el tamaño de la estructura producto_t? Eso es parte del problema que se está dando, ya que en el ejemplo la estructura ocupa 6 bytes, pero empaquetado es unsigned short int (2 bytes) o unsigned int (4 bytes). No es que procedencia y basura estén escondidos o perdidos; están ahí, pero empaquetado no tiene el tamaño suficiente para mostrarlos. Se podría cambiar a un tipo de dato de 64 bits (aunque ya no se podría usar el especificador %X para imprimirlos sino que habría que recurrir a la macro PRIX64), pero de nuevo, esta no es una manera aconsejable de usar union y estamos adivinando qué tamaño tendrá la estructura. Lo mejor, reitero, sería usar un unsigned char* que apunte a la dirección de vec.producto, y vaya recorriendo por sus bytes y mostrándolos. O en todo caso, si no quieres usar punteros, puedes hacer que empaquetado sea arreglo de caracteres como lo hacías originalmente: Código
y vas iterando, desde 0 hasta sizeof(s_producto) - 1: Código aunque, dado que la arquitectura x86 (y x86-64) es little-endian (es decir, se almacena primero el byte menos significativo, o más a la derecha), los datos te aparecerían de manera inversa a la que probablemente esperarías, por lo que tal vez sea mejor mostrarlos al revés, iterando desde sizeof(s_producto) -1 hasta 0. Título: Re: campos de bits Publicado por: MAFUS en 29 Septiembre 2023, 12:23 pm Motivado por la respuesta de RayR decidí encontrar los bits que había perdido.
Aquí están Código
Y la respuesta es: Código: tamanno del mapa de bits: 6 bytes Título: Re: campos de bits Publicado por: RayR en 3 Octubre 2023, 02:48 am Como aquí estamos poniendo sólo ejemplos ilustrativos, creo que es válido ver la representación interna de las variables de esa manera, pero reitero que lo mejor es usar un unsigned char* y recorrer con él los bytes de la estructura, ya que, en general, en C y C++ no es válido acceder a una variable mediante un puntero a un tipo distinto. Una excepción son los punteros a char (con o sin signo), los cuales sí se pueden usar con variables de otros tipos. Hay otras excepciones (por ejemplo, un puntero unsigned/signed puede acceder a datos signed/unsigned, siempre que sean del mismo tipo) pero son muy pocas.
Romper esa regla resulta en comportamiento indefinido (https://es.wikipedia.org/wiki/Comportamiento_indefinido) o UB (https://en.wikipedia.org/wiki/Undefined_behavior), y en ese caso los compiladores pueden hacer prácticamente lo que quieran. Pudiera parecer estos son sólo problemas teóricos, pero no es así, pues constantemente se encuentran problemas derivados de estos malos usos. Aquí pongo unos ejemplos: Código
En GCC o MinGW, si se compila con -O2 o superior, la salida es: Código: base : 00000000 00000000 Lo que pasó es que el compilador decidió optimizar el programa y dejar sólo esto: Código es decir, directamente imprime los valores con los que base fue inicializado. Puesto que *prod y base tienen tipos distintos e incompatibles, no es válido modificar base mediante ese puntero, así que el compilador decide que base jamás fue modificado luego de su inicialización. Otro ejemplo casi igual pero con un resultado todavía peor: Código
de nuevo, GCC y MinGW arrojan esta salida: Código: base : 00000000 00000000 por las mismas razones de antes. Y esto no se limita a GCC, sino que cada compilador tiene sus "detalles". Por ejemplo, hace años alguien reportó un posible bug en clang. El ejemplo que puso era más o menos así: Código
con -O1 o superior, en clang da esta salida: Código: Esto deberia ser 0: 1 Edit: corrijo números de línea se supone que siempre debería ser 0 porque la última instrucción que se ejecuta antes del printf es la de la línea 12, pero el compilador hace lo que quiere, porque la línea 10 contiene UB (¡a pesar de que nunca se va a ejecutar, ya que la condición del if forzosamente será falsa!). Si se cambia el short* por char*, ahí la modificación ya es válida y el programa imprime 0, como se esperaba. Estos ejemplos pueden parecer muy rebuscados o "artificiales", pero la realidad es que normalmente ese tipo de problemas se encuentran al estar trabajando en programas reales. Obviamente uno no va poner programas enteros en los foros o en reportes de bugs, así que se busca crear un ejemplo lo más básico y simplificado posible que muestre el problema, pero estas cosas ocurren todo el tiempo en programas de todo tipo y tamaño, y de hecho hay tanto debate al respecto, que hay muchas conferencias hablando sobre el tema y justificando (o no) lo que hacen los compiladores. Como muchos otros programadores, no soy fan de optimizaciones tan agresivas como éstas, y para mí, el caso de clang debería considerarse un bug (si experimentamos un poco con el código, obtenemos resultados aún más raros), pero sus desarrolladores no piensan así, y nunca lo arreglaron. Técnicamente, el estándar de C (y C++) les da la razón. El caso es que esté o no uno de acuerdo, los compiladores hacen este tipo de cosas cuando encuentran código que viola el estándar, así que es mejor intentar apegarse a las reglas. Título: Re: campos de bits Publicado por: MAFUS en 4 Octubre 2023, 12:27 pm Es correcto todo lo que dices. Aunque también es cierto que si usas volatile te quitas de encima esas molestas optimizaciones del compilador. Y oye, que para eso está la volatile.
Y sí, lo he probado con el código que pasaste de ejemplo con -O2, con volatile y te muestra los resultados. Título: Re: campos de bits Publicado por: RayR en 5 Octubre 2023, 19:42 pm Sí, además de su función principal, volatile puede ser útil al depurar, y en casos como éste, también puede ser válido. Ojo, en ningún momento pretendo decir que todos los ejemplos de los primeros mensajes estén "mal". Se entiende que son cosas que uno normalmente no usaría para cosas más reales (aunque no está de más decir que, aún en códigos simples de ejemplo, violar las reglas nos puede traer más de una sorpresa desagradable).
Pero hay gente, incluso aquí en el foro, que a veces intenta ya no sólo experimentar sino implementar cosas de bajo nivel en sus programas. Por ejemplo, hace poco un usuario estaba haciendo algo similar a un editor hexadecimal. Mis recomendaciones, y lo que sigue, son sobre todo para quien quiera hacer cosas así, "reales". Primero, para saber más sobre el tema, se puede googlear "strict aliasing" que es como se conoce a la regla de la que hablo en mi mensaje anterior. Y hay unas cosas que es fundamental entender: 1) Esto lo recalco de nuevo, cuando un programa hace cosas que el estándar cataloga como "undefined behaviour", el resultado, y las acciones del compilador, se vuelven impredecibles y éste puede hacer lo que quiera y a menudo hace cosas distintas en versiones distintas. Otra referencia más en FAQ de C: https://c-faq.com/ansi/undef.html . 2) Las optimizaciones que el compilador hace son totalmente dependientes del contexto. Dado un conjunto de líneas, su optimización puede ser totalmente distinta si las metemos en un for, o en dos for anidados, o si hay un if, o si las variables a las que acceden son globales o locales, o parámetros recibidos, o si la función es inline, etc., casi cualquier cosa lo altera. 3) Si el programa está escrito correctamente, un compilador jamás va a cambiar su comportamiento observable y su funcionamiento. Los ejemplos anteriores fallan únicamente porque violan el strict aliasing. Si se cambian de manera que respeten esta regla (usando unsigned char*, por ejemplo) se arreglan de forma permanente. El calificador volatile no es realmente solución; en todo caso, es un parche temporal. De nuevo, si se googlea sobre el tema, van a salir muchos resultados de fuentes confiables, ninguno de los cuales sugiere siquiera a volatile como solución (los pocos que lo mencionan básicamente dicen: "no arregla el problema. Si te sirve es suerte"). El propio Linus Torvalds dijo lo siguiente cuando alguien supuso que volatile servía para arreglar estos problemas: "sí, volatile podría funcionar, pero es como matar una mosca con una bomba atómica (...) y no hay garantía de que el compilador no haga las optimizaciones, aún con acceso volatile, así que de todas formas es un punto irrelevante". En términos prácticos, con volatile el compilador desactiva optimizaciones relacionadas con el acceso a las variables calificadas de esta manera (ya no las elimina, y para cada lectura, se traen sus valores de la memoria, etc.), pero eso no tiene nada que ver con el aliasing, lo que pasa es que en algunos contextos, como mis ejemplos, la única optimización que hace el compilador al encontrar UB coincide, de forma fortuita, con las que deshabilita volatile. Pero hay toda una gama de cosas que el compilador puede hacer (eliminar instrucciones que producen UB, reordenarlas, etc.) que vuelven a romper los programas a pesar de usar este calificador. No quiero que salga un mensaje aún más largo de lo que es, y no le veo sentido a poner más ejemplos (siempre se puede encontrar un "parche" que arregle de momento el programa, y luego poner un nuevo ejemplo con las instrucciones reacomodadas/cambiadas de manera que el parche no funcione, y luego otro parche, y así nos pasamos la vida, porque no estamos arreglando el problema) pero nada de esto es hipotético, pasa en la realidad. Por no hablar de otros problemas, como la alineación incorrecta. De esto también hay ejemplos reales, incluso en x86-64 (donde algunas instrucciones producen errores de violación de acceso si los datos no tienen una alineación específica), y derivados de violar el strict aliasing. La única solución general es seguir las reglas del lenguaje, en este caso, respetar el strict aliasing. Usar punteros char o memcpy (con ciertas precauciones) son maneras válidas. De esta manera nos quitamos de cualquier preocupación. Los programas van a funcionar siempre, en cualquier compilador que acepte la versión del lenguaje elegido, y con cualquier nivel de optimización. Si forzosamente necesitamos acceder a datos mediante punteros a tipos incompatibles (en unas pocas ocasiones puede ser conveniente), lo que se debe hacer es compilar con -fno-strict-aliasing o equivalente. Esto le dice al compilador que vamos a violar el strict aliasing, y así sabe exactamente que debe desactivar las optimizaciones específicas a esta regla (y no otras que podrían o no arreglar estos problemas en un contexto dado). Y todo esto sin los problemas de rendimiento que trae volatile. |