La pareja perfecta

 


En la Vida Real ™ han vivido parejas de éxito, como Bonnie and Clyde, el Gordo y el Flaco, el Coyote y el Correcaminos, Tom y Jerry, Piolín y el gato malo maloso, Rajoy y Zapatero… Sí, ya sé, las cuatro últimas parejas no pertenecen a la Vida Real ™, pero como si lo fueran. ¿O no has vivido, y quizás todavía vivas, todas las escenas que estos dúos sufren en sus episodios y te partes de risa viéndolos por la caja tonta? Por lo tanto, son parejas de la Vida Real ™. Q.E.D. [Que no significa “Que Enpaz Descanse”, sino “Quod Erat Demonstrandum», aunque alguna de esas parejas se merezcan la primera acepción].

Como locos informáticos que somos, nosotros tenemos otros dúos: Linux y Windows, programar con dos monitores, tener dos teclados y confundirte de teclas cuando cambias entre uno y otro, e incluso, a veces, comprar un software y disponer de dos licencias, una para tu ordenador fijo y otra para tu portátil.

Y como jodíos developers programadores que somos, usamos Visual Studio (evidentemente habrá quien no lo haga, quizás muchos, así que libéreseme –coñis, el Word entiende el palabro- de lo evidente), y a veces, sobre todo para los pobrecitos que seguimos con C++ (aunque luego ese C++ lo usemos desde .NET mediante Interop, pero una compañía no debe dejar de lado ni a los Builderos, Delphínicos, Visual Basiceros de antes, gnueros, o incluso fortraneros, por qué no), nos encontramos con que hay cosas dentro de Visual Studio que no funcionan todo lo bien que debieran, como el IntelliSense que, si bien en C# marcha de forma impecable, en C++, sobre todo si picas rápido, cuando quiere enterarse, tu ya has terminado la clase y hasta el programa entero.

Ahora, según el guión, toca poner una pantalla del editor de Visual Studio con C++ tal y como lo trajo al mundo Microsoft (para los curiosos, el código que se muestra pertenece a mi zxDelTemp):

clip_image002

Y ahora vamos a poner otra:

clip_image004

¿Veis la diferencia? Yo sí, no sé vosotros. Yo le doy un vistazo a la segunda pantalla y reconozco casi lo que es cada cosa al primer vistazo. Fijaros arriba, que hay dos ventanitas extra. Sirven para saltar a un método en concreto del fichero abierto, y funciona perfectamente, no como las que trae Visual Studio, que a veces se van por los Cerros de Úbeda o más allá.

Y ahora volvemos al IntelliSense y su variante multicolor, como el mundo de la Abeja Maya. Imaginaros que tenemos un objeto o un puntero a objeto y, al picar el punto, se abriera un IntelliSense que te ofreciera, no sólo los métodos de ese objeto, sino posibles “adelantos” de lo que quieres picar, y que encima, con una probabilidad bastante alta, ese “adelanto” fuera “el adelanto” justo que te hace falta. Y encima, si el objeto es un puntero a objeto, te cambie el punto por la flechita.

Sigue imaginando. Imagina que comenzaras a picar unas comillas para un texto y que el entorno te pusiera las de cierre, te cierre los paréntesis y te de una indicación visual de a qué paréntesis de apertura se corresponde. Lo mismo con las llaves. Imagina que empezaras a picar un texto, cualquier texto y encima con algún error tipográfico, que el editor te ofreciera posibilidades y que una de esas posibilidades fuera la correcta, pulsaras enter y tuvieras la palabra correcta y corregida.

Imagina que te equivoques al mecanografiar, sobre todo en los comentarios, pero a veces en el código fuente, ya sea porque el editor ha fallado en ofrecerte posibilidades (que a veces lo hace, igual que ofrecerte mal el cierre de los paréntesis) y que piques un símbolo que no existe. ¿Qué tal que el editor te lo subrayara como erróneo? ¿Qué tal si pudieras añadir el español o cualquier otro idioma a la lista de palabras a corregir, aparte de los símbolos incorrectos?

¿Programas con C#? ¿Has visto las opciones de refactorización que tiene? ¿Te gustaría tener en C++ las mismas o más?

Y por último, imagina por un momento que no añadiera apenas sobrecarga al Visual Studio, y que no notaras diferencia de rendimiento entre tenerlo y no.

¿Fantasías animadas de ayer y de hoy, como algunos de las parejas perfectas con las que empezaba esta entrada? ¿Eso no existe?

Pues sí, sí que existe. Se llama Visual Assist, la empresa se llama TWhole Tomato, y tiene, a fecha de hoy, el módico precio de 99$ (Dólares USA, no Euros) si lo compras sin mantenimiento. Tiene más cosas aparte de las comentadas, y no sólo sirve para C++, sino que también funciona con otros lenguajes, la licencia es válida para instalar en dos equipos propios, y yo, personalmente, no puedo pasar sin él. Y os prometo que no es publicidad encubierta, quien me conozca sabe que no miento.

x64 vs x86: ¿Por qué un programa de 64 bits ocupa casi el doble de memoria que uno de 32?

Esta entrada tiene su origen en una pregunta que hizo Jume en el grupo de Generales del servidor de Tella sobre el título; a ella dieron cabal respuesta el propio Tella y José Antonio Quílez, aunque de forma bastante resumida, lo que fue motivo para que el que suscribe iniciara una disertación sobre el tema. Finalmente, gracias al empujón de Ramón Sola, y a la depuración de ideas que el propio hilo ha generado, he decidido poner una entrada aquí sobre el tema.

¿Por qué un programa de 64 bits ocupa casi el doble de memoria que uno de 32 si el código fuente es el mismo?

En 32 bits la frontera de RAM son 4 bytes, es decir, un salto «nativo» de dirección salta 4 bytes, de, por ejemplo, 16 a 20. Puede saltar de 16 a 17 sin problemas, pero eso requiere ciertos ajustes internos del microprocesador y de la RAM (dado que las RAM modernas también están vivas) que llevan un importante consumo de tiempo. Es como si en tu casa pasaras corriendo por un pasillo que tuviera puertas a los lados, y que una de cada 4 estuviera siempre delante. Si pasas de 4 en 4, sólo corres en línea recta. Si pasas de 2 en 2, tienes que frenar en la segunda, girar, y volver a acelerar. Y encima, la RAM (la puerta), está cerrada si no es la de enfrente, por lo que también has de esperar a que se abra. Trasladándolo al micro, es más fácil y rápido leer una ráfaga de 4 bytes de un golpe que 1 lectura de 1, aunque de esos 4 bytes sólo vayas a usar 1.

En 64 bits ocurre lo mismo, pero ahora son 8 puertas en lugar de 4. Y aunque necesites 1, es más rápido leer 8 y descartar 7 que hacer todos los cambios para poder acceder fuera de la frontera de 8 bytes, leer 1 y luego deshacer los cambios para volver al estado anterior.

Lo que hacen los compiladores es optimizar esos accesos, de forma que no siempre se necesita 1 y se toman 8 (si no un programa de 64 bits gastaría justo el doble de memoria que uno de 32), a veces se necesitan 4, o 5, o 6 o 300. En el último caso, 300mod8 es 4, nos faltan 4 bytes en 64 bits y 0 en 32, por lo que en 64 bits tenemos que coger 304 bytes, desaprovechado 4 bytes de memoria, con lo que estamos gastando 4 bytes más en 64 que en 32. Este es uno de los factores que hacen que un programa de 64 bits gaste más memoria que uno de 32.

Luego están los punteros. Los punteros, aunque a nadie le gustan (sin embargo, a mi me encantan), son la vida de un ordenador. Si no se hubiera inventado tanto su concepto como su implementación, no existirían los ordenadores. Un puntero es una variable situada en memoria que guarda la dirección de otro bloque de memoria.

En el caso anterior, y para 32 bits, hemos asignado 300 bytes de RAM. Tenemos por un lado los 300 bytes en un hueco de la memoria, y por otro tenemos 4 bytes en nuestro programa que apuntan a la dirección base de esos 300 bytes. Cuando nuestro programa necesita algo de esos 300, se va al puntero, mira su contenido y, si quiere acceder a la posición 4 dentro de esos 300, suma 4 al valor del puntero y lee en la dirección física de destino (y esas lecturas serán mucho más rápidas si están alienadas a 4 bytes que si no lo están).

En 64 bits, ese puntero ocupa 8 bytes en lugar de 4, el bloque de 300 es de 304, y el proceso de funcionamiento es idéntico. Y ese puntero ocupa 8 bytes no porque la memoria se lea de 8 en 8, sino porque mientras que la arquitectura de 32 bits necesita 4 bytes para guardar todas las direcciones posibles, la de 64 necesita 8 para hacer lo mismo y, por tanto, nuestros punteros tienen que tener ese tamaño.

Por lo tanto, en 64 bits cada puntero ocupa el doble que en 32 bits. Si una aplicación usa 123 punteros en un momento dado, esos punteros, alineación aparte (porque su propio «hueco» en la memoria también está alineado a 4 u 8 bytes), ocuparán 123×4 en 32 bits y 123×8 en 64 bits.
El hecho de que la alienación y los punteros sean iguales entre sí tiene que ver con la propia arquitectura física de la máquina; la idea básica es que se producen ciertas optimizaciones dentro de la construcción del hardware si la alineación de datos se produce con el mismo tamaño que el del ancho de la dirección. Para hacernos una idea, es como si un microprocesador y una memoria estuvieran construidos con muchos bloques predefinidos que tuvieran 4 u 8 puertas respectivamente de su arquitectura, con dos de ellas una enfrente de la otra. Y en algunos casos, hasta se ahorran pistas sobre el circuito impreso al no cablear las dos o cuatro líneas de dirección más bajas (Addr0 y Addr1, Addr2 y Addr3).

Pero el tema no es tan simple, ya que luego ese puntero de 4/8 bytes apunta realmente al final de una estructura cuyos valores siguientes son el bloque de memoria asignado y cuyos valores anteriores son otros números que Windows/Linux necesitan para gestionar la memoria, ya que de hecho la memoria se asigna habitualmente en bloques de 4K y luego el runtime correspondiente genera una lista enlazada que reparte esa memoria en los bloques pedidos (con lo que todavía hay más punteros), y a su vez esas direcciones son relativas y móviles dentro de la memoria física, de forma que el kernel sea capaz de moverlos en la memoria física (todavía más punteros), de modo que al final el valor medio de aumentos es de entre un 40% y el doble, lo que no deja de ser una tasa completamente estimativa.

En el tema de los punteros también intervienen más factores, como lo inteligente que sea el compilador sustituyendo datos estáticos por su valor directamente, los algoritmos que el programador haya decidido implementar, cuántos bloques de memoria se asignen y cómo, y la propia gestión de memoria tanto del Kernel como del runtime, amén del grado de fragmentación de la misma.

Como ejemplo típico está el uso de un array disperso, que es un array que crece en bloques. Imaginemos un grano de 16, cuando el array tenga 16 elementos y pase a 17, se vuelve a crear un nuevo bloque de 16 elementos y se ocupa uno. Hemos añadido al menos tres punteros más al sistema (aparte de los del gestor de memoria). Supongamos que ese array disperso sea un array de punteros…Casi nada, la de punteros que hemos creado… Y una aplicación más o menos seria (Word, IE, Outlook) muy bien podría tener varios miles de ellos…

También está el tema del tamaño de los datos, aunque aquí las especificaciones sí que han sido un poco comedidas. Pensemos que una variable de tipo bool ocupa en una arquitectura de 32 bits, 4 bytes, y 8 en una de 64. Tengamos un array disperso de booleanos y nos daremos cuenta de cómo crece el gasto de memoria.

Luego está el tema del juego de instrucciones, en x64 está más optimizado y hay más que en x86… y, no es lo mismo un programa compilado en 32 bits que en 64. En general en 64 bits se generan menos instrucciones y estas son más rápidas… con lo que la relación de tamaños de un programa guardado en disco no es lineal. Quizás los datos guardados o fijos dentro del ejecutable ocupen justo el doble, pero el código, al ser menor, hace que ocupen algo menos, pero como los códigos de operación de 64 bits son más largos que los 32 bits, el tema se complica hasta límites insospechados.

Pongamos un ejemplo práctico: mi zxDelTemp, que tiene exactamente el mismo código fuente MFC tanto en su versión de 32 como de 64 bits. La compilación que uso para 32 bits ocupa 237.680 bytes, mientras que la de 64 bits ocupa 476.672 bytes, aproximadamente un 68,7% más.

SysWoW64

Nos queda el tema del SysWoW64, es decir, tener un programa de 32 bits ejecutándose dentro de un entorno de 64. Aunque nunca lo he mirado en detalle, tengo unas cuantas cosas claras, ya que son completamente lógicas.

Windows suministra un entorno virtual de 32 bits sobre uno de 64, es decir, las aplicaciones de 32 bits se ejecutan en un pseudo código de 32 bits, pero el núcleo es de 64 bits, por lo que cuando una aplicación pide un recurso al sistema (abrir un fichero, por ejemplo), Windows crea una serie de estructuras (completamente llenas de punteros a muchos sitios), estructura que es pasada al subsistema de 32 bits convertida a su tamaño (con lo que duplicamos la misma estructura, una de 64 y otra de 32, y si no la duplicamos –realmente no lo tengo claro-, estamos al menos usando una estructura de 64 bits –ya sabemos, alineada a 8, con punteros de 8 bytes, etc.-, en un sistema de 32).

Cuando un programa de 32 bits reserva memoria, Windows ha de asignar un bloque con formato de 64 bits aunque luego dentro del wow64 haya un subgestor de memoria local, pero el hecho es que también estamos trasteando con punteros de 64 bits…

Finalmente, y aunque la aplicación se esté ejecutando en un entorno de 32 bits, todas las estructuras del núcleo asociadas a dicha aplicación son de 64 bits, amén de que seguro algunas (si no todas) se encuentran duplicadas dentro del subsistema de 32 bits, porque no se entiende cómo un programa que espera un puntero de 4 bytes ha de poder procesar uno de 8.

Por lo tanto, las aplicaciones de 32 bits corriendo dentro de un sistema de 64, también consumen más memoria que si lo estuvieran en uno de 32; ciertamente en muchos casos se ejecutan sensiblemente más rápido (por ejemplo SQL Server), pero esto se debe a que se produce menos fragmentación de memora, hay más disponible y por tanto se asigna más rápidamente, y las instrucciones de 32 bits dentro de un x64 se ejecutan mucho más rápidamente que bajo un x86.

Resumen

Resumiendo, el código x64 resulta de mayor tamaño tanto en disco como en memoria debido a los siguientes factores (quizá se me escape alguno):

· El juego de instrucciones de x64 ocupa el más que el de x86.

· Los punteros ocupan justo el doble.

· La alineación de memoria es a 8 bytes en lugar de a 4.

· Muchos datos tienen un tamaño doble, pero no todos.

· Los metadatos también ocupan más.