lunes, 8 de diciembre de 2008

STL generate

Si usted tiene una función que genera números/valores/instancias (ej: un generador de números aleatorios, o identificadores) puede usar el algoritmo generate de la STL para crear una secuencia de objetos. Por ejemplo, para generar 100 números (pseudo)aleatorios de 0 a 1:
#include <algorithm>
#include <vector>

double uniform_random()
{
  return (rand() % 10001) / 10000.0;
}

int main()
{
  std::vector<double> v(100);
  std::generate(v.begin(), v.end(), uniform_random);
  return 0;
}
El algoritmo generate recibe dos iteradores (inicio y fin), y llama a la función especificada para cada una de las posiciones que recorre:
template<typename ForwardIterator, typename Generator>
void generate(ForwardIterator first, ForwardIterator last,
              Generator gen)
{
  for (; first != last; ++first)
    *first = gen();
}
Los algoritmos de la STL son genéricos porque utilizan la aritmética de punteros. Los iteradores sobrecargan los operadores para poder ser utilizados como punteros. Así un algoritmo puede ser utilizado con los contenedores de la STL (vector) o con los viejos y tan queridos arreglos (y punteros) de C:
#include <algorithm>

double uniform_random()
{
  return (rand() % 10001) / 10000.0;
}

int main()
{
  double v[100];
  std::generate(v, v+100, uniform_random);
  return 0;
}

martes, 19 de agosto de 2008

¿Preincrementar o postincrementar?

Hace mucho que quería escribir un post tan inútil como este. La duda que siempre me pasaba por la cabeza era la siguiente: ¿Será exactamente el mismo código ensamblador el que genera el compilador cuando utilizamos el preincremento o el postincremento (sin utilizar el valor de retorno)? Tenía la seguridad de que así debía ser (una optimización tan básica no podía ser dejada de lado), pero me faltaban las pruebas (en ensamblador) para verificarlo.

Recordando un poco, el preincremento
int a = 0;
int b = ++a;
hace a=1 y b=1, mientras que el postincremento
int a = 0;
int b = a++;
hace a=1 y b=0, esto significa que b obtuvo el valor de a anterior al incremento.

Aquí estamos utilizando el valor de retorno del operador incremento. El código ensamblador es distinto para cada caso. Vamos a echarle una mirada (antes le recomiendo ver este excelente artículo para comprender más sobre el stack y las convenciones de llamadas). En el preincremento el código ensamblador (generado por GCC 3.4.5 para i386) es:
subl  $8, %esp          // reservamos 8 bytes en el stack (para variables locales)
movl  $0, -4(%ebp)      // a   = 0
leal  -4(%ebp), %eax    // eax = &a
incl  (%eax)            // *eax= (*eax) + 1
movl  -4(%ebp), %eax    // eax = a
movl  %eax, -8(%ebp)    // b   = eax
En el postincremento
subl  $8, %esp          // reservamos 8 bytes en el stack
movl  $0, -4(%ebp)      // a   = 0
movl  -4(%ebp), %edx    // edx = a
leal  -4(%ebp), %eax    // eax = &a
incl  (%eax)            // *eax= (*eax) + 1
movl  %edx, -8(%ebp)    // b   = edx
donde se ve que el registro edx se utiliza para guardar el valor que tenía a antes del incremento para luego asignárselo a b.

Pero la pregunta original es, ¿qué pasa si no usamos el valor de retorno? ¿el código de ++i o i++ es igual? Debería serlo, y de hecho, lo es. Pero como veremos más abajo, esto sólo se cumple si utilizamos tipos de datos built-in (int, double, long, etc.). Miremos el siguiente código:
void func() { }
void pre () { for (int c=0; c<8; ++c) { func(); } }
void post() { for (int c=0; c<8; c++) { func(); } }
int main()
{
  pre();
  post();
  return 0;
}
Básico, un for pero con las dos variantes posible de incremento. El código generado para ambos casos (tanto para la función pre como post) es el siguiente:

  pushl %ebp              // guardar el viejo puntero a la base del stack
  movl  %esp, %ebp        // establecer la nueva base del stack
  subl  $4, %esp          // guardar 4 bytes en el stack (para variables locales)
  movl  $0, -4(%ebp)      // c = 0
L3:
  cmpl  $7, -4(%ebp)
  jg    L2                // si c > 7 entonces ir a L2
  call  _func             // llamar a la función func()
  leal  -4(%ebp), %eax    // eax = &c
  incl  (%eax)            // *eax= (*eax) + 1
  jmp   L3                // repetir yendo a L3
L2:
  leave                   // restaurar la base del stack (popl %ebp)
  ret                     // retornar al punto de llamada
Realmente es indiferente usar cualquiera de los dos tipos de incremento, salvo, en los tipos definidos por el usuario. C++ ofrece un soporte para tipos de usuario igual a los built-in (bueno, no del todo, pero lo están solucionando). Nos da la posibilidad de sobrecargar los operadores de nuestros propios tipos (clases). Por ejemplo:
class tipo {
  int x;
public:
  tipo(int y) : x(y) { }
  tipo(const tipo& y) : x(y.x) { }
  // preincremento
  tipo& operator++() {
    ++x;
    return *this;
  }
  // postincremento
  tipo operator++(int) {
    tipo tmp(*this);
    ++x;
    return tmp;
  }
  bool operator<(int y) const { return x < y; }
};

void func() { }
void pre()  { for (tipo c=0; c<8; ++c) { func(); } }
void post() { for (tipo c=0; c<8; c++) { func(); } }

int main()
{
  post();
  return 0;
}
No colocaré el código ensamblador por desbordar belleza, pero el código generado en este caso es distinto: cada incremento llama a la función correspondiente a su operador. Esto es debido a que ambas implementaciones varían considerablemente. Por ejemplo, el postincremento necesita de una instancia extra de tipo para poder devolver el anterior valor de this, en cambio, el preincremento devuelve una referencia al mismo this (sin necesidad de hacer una copia).

Como dato curioso, algo interesante ocurre al utilizar las optimizaciones del GCC. Si compilamos este último programa con el parámetro -O3, vamos a ver que todas las funciones correspondientes a la clase tipo desaparecen, y todo el código resultantes es inline, dando como resultado un código tan óptimo como si hubiéramos utilizado un int en vez de nuestra clase tipo.

¿Cómo obtengo el código ensamblador desde un archivo C/C++?
Con el compilador gcc, hay que utilizar el parámetro -S:
g++ -S -o archivo.s -c archivo.cpp
En archivo.s queda el código ensamblador (sintaxis AT&T).

martes, 24 de junio de 2008

Excepciones, RAII y auto_ptr

Una de las mejores formas de realizar un control de errores es por medio de excepciones. Lamentablemente, si no se toman las precauciones adecuadas, se pueden obtener leaks de algunos recursos. Por ejemplo, imagine el siguiente caso:
int main()
{
  try {
    char *ptr = new char[256];
    double *matrix = new double[100000*100000]; // 74 Gigabytes
    // ...
    delete matrix;
    delete ptr;
  } catch (const std::bad_alloc& e) {
    std::cout << "No hay memoria suficiente" << std::endl;
  }
  return 0;
}
Aquí, con el segundo new una excepción bad_alloc es lanzada. El problema es que la memoria asignada con el primer new nadie la libera. Esto es un memory leak. Una opción para solucionarlo es mediante un bloque finally "a la Java":
int main()
{
  char *ptr = NULL;
  double *matrix = NULL;
  try {
    ptr = new char[256];
    matrix = new double[100000*100000];
    // ...
    delete matrix;
    delete ptr;
  } catch (...) { // finally
    if (matrix) delete matrix;
    if (ptr) delete ptr;
  }
  return 0;
}
Claro que esto no hace al manejo de errores una tarea transparente.

En C++ se puede utilizar la técnica RAII (la adquisición de recursos está en la inicialización), lo que ayuda a evitar leaks. La idea básica detrás de esto es que los constructores obtengan recursos, y los destructores los liberen. Al ocurrir una excepción, se van llamando los destructores de los objetos creados.

Para solucionar nuestro problema, podemos utilizar la clase auto_ptr que se encuentra en <memory>. Un auto_ptr guarda un puntero que es liberado automáticamente en el destructor.
int main()
{
  try {
    std::auto_ptr<char> ptr(new char[256]);
    std::auto_ptr<double> matrix(new double[100000*100000]);
    // ...
  } catch (const std::bad_alloc& e) {
    std::cout << "No hay memoria suficiente" << std::endl;
  }
  return 0;
}
Los deletes no son necesarios. Al salir del ámbito del bloque try, ya sea normalmente o por excepción, se llamará al destructor de los auto_ptrs creados, así no se incurre en ningún tipo de leak.

Advertencia: Los auto_ptr deben ser utilizados sabiendo cuál es su verdadero funcionamiento: La copia de un auto_ptr a otro no los deja equivalentes. La última copia se queda con el puntero, los demás lo van perdiendo, y el delete lo invoca el último auto_ptr destruido. Por ejemplo:
{
  std::auto_ptr<Persona> otra_persona;
  {
    std::auto_ptr<Persona> una_persona(new Persona);
    otra_persona = una_persona;
    // ahora "una_persona" apunta a NULL
  }
} // aquí la persona es destruida

Pregunta: ¿Por qué no usar objetos alojados en el stack en vez del heap para evitar los leaks? Porque el stack es mucho más pequeño. Por ejemplo, el siguiente código provoca un SIGSEGV ya que el arreglo no entra en el stack:
double matrix[1000000];
Pero sí entra en el heap:
double *matrix = new double[1000000]; // 7.6 Megabytes

sábado, 10 de mayo de 2008

Compilando archivos

Suponiendo que conoce qué es un archivo de código fuente, código objeto, un ejecutable .exe, y el código máquina; voy a pasar a explicar algunos comandos del GCC para compilar.

Retomando la idea del anterior post, imagínese que creamos un archivo timemark.h para darle un poco de portabilidad a todo este asunto de medir el desempeño de una rutina. Las declaraciones de timemark.h pueden ser usadas para crear un pequeño programa ejemplo1.c que sea independiente de la plataforma (por lo menos entre Windows y GNU/Linux).

Vamos a probar algunas operaciones sobre la línea de comandos para ver cómo compilar este archivo. Inicialmente, la opción más sencilla es utilizar directamente "gcc ejemplo1.c", esto nos da como resultado el archivo "a.exe" o "a.out". Veamos (teniendo timemark.h y ejemplo1.c en un directorio C:\ejemplo1):
C:\ejemplo1>gcc ejemplo1.c
C:/Temp/ccMjdaaa.o:ejemplo1.c:(.text+0x31): undefined reference to `get_timemark'
C:/Temp/ccMjdaaa.o:ejemplo1.c:(.text+0x3c): undefined reference to `get_timemark'
C:/Temp/ccMjdaaa.o:ejemplo1.c:(.text+0x4e): undefined reference to `timemark_diff'
collect2: ld returned 1 exit status
Como podrá ver, recibimos un error por las funciones no definidas get_timermark y timemark_diff. Esto se debe a que utilizando solamente "gcc ejemplo1.c", se intenta enlazar el código de ese único archivo en el ejecutable. Tenemos algunas opciones intermedias antes de crear el .exe final. Por ejemplo, podemos crear un archivo que contenga el código objeto ya compilado (pero únicamente del archivo en cuestión ejemplo1.c):
C:\ejemplo1>gcc -o ejemplo1.o -c ejemplo1.c
Así obtenemos el ejemplo1.o (código objeto). En este caso no recibimos errores ya que ejemplo1.o hace referencias a las funciones get_timermark y timemark_diff, pero no necesita su implementación específica. Cuando creamos el ejecutable final (.exe) es cuando realmente necesitamos el código de todas las funciones invocadas.

El archivo que falta es timemark.c. Del mismo modo, podemos compilarlo con el comando:
C:\ejemplo1>gcc -o timemark.o -c timemark.c
Ahora resta unir ambos archivos objetos en un único ejecutable, con un único punto de acceso (main):
C:\ejemplo1>gcc -o ejemplo.exe ejemplo1.o timemark.o
Y listo. Por ahora, lo aprendido son estos dos comandos:
gcc -o archivo.o -c archivo.c
gcc -o archivo.exe archivo1.o archivo2.o archivo3.o ...
El primero compila el código fuente para generar un archivo objeto, y el segundo comando reune (enlaza, linkea) el grupo de archivos objetos en un ejecutable único.

De todas formas, es bastante tedioso escribir estos comandos cada vez que queremos compilar. Para resolver el problema podemos utilizar los llamados Makefiles. Claro, lo más interesante queda para un futuro post.

sábado, 1 de marzo de 2008

Medir el tiempo de una rutina

¿Alguna vez se preocupó por la velocidad con la que corre su programa? ¿No? Entonces usted es un candidato perfecto para jugar al Java. En caso contrario, voy a explicarle un pequeño código que utilizaremos en próximas entregas para medir el tiempo de ejecución de determinadas rutinas.

Ya lo dijo alguien: "La optimización prematura es la raíz de todos los males". No hay nada más cierto, aunque también es verdad que hacer algo simple de la peor forma posible, es la causa de otros grandes males. Con esto quiero decir que debería intentar hacer las cosas de la forma más simple y más óptima que usted conozca (dándole mayor importancia a la simplicidad del código); luego se preocupa por "darle velocidad" a la rutina que provoca el cuello de botella (en próximos posts veremos cómo usar gprof para detectarlo).

La forma de calcular el tiempo de CPU que toma una función es muy simple:
  • tomamos el valor del reloj antes de realizar la llamada (t_ini),
  • llamamos a la rutina en cuestión, y
  • tomamos nuevamente el valor del reloj (t_fin).
La diferencia entre t_fin - t_ini nos da el total de tiempo que tomó: 1) hacer la llamada a la rutina, 2) que esta haga su trabajo, 3) que devuelva el resultado.

Ahora hay algunos pequeños detalles de implementación. Por ejemplo, ¿qué función usar para tomar el tiempo del reloj? Y más importante, ¿qué precisión obtenemos con dicha función?

Para tomar el tiempo podemos usar la rutina clock(), que devuelve el tiempo aproximado de CPU que transcurrió desde que nuestro programa fue iniciado, dicho tiempo representado en un valor de tipo clock_t: un valor entero que indica una cantidad de "tics" de reloj.

La precisión que tenemos con dicha rutina es de CLOCKS_PER_SEC (tics de reloj por segundo), lo que significa que por cada segundo que pasa, la función clock() nos devolverá CLOCKS_PER_SEC unidades más que el valor anterior. En MinGW, CLOCKS_PER_SEC es igual a 1000, pero es mejor no fiarse de esto, ya que en otras plataformas dicho valor varía. Inclusive, según POSIX, la constante CLOCKS_PER_SEC debería ser 1000000.

Veamos algo de código:
#include <stdio.h>
#include <time.h>

int main(int argc, char *argv[])
{
  clock_t t_ini, t_fin;
  double secs;

  t_ini = clock();
  /* ...hacer algo... */
  t_fin = clock();

  secs = (double)(t_fin - t_ini) / CLOCKS_PER_SEC;
  printf("%.16g milisegundos\n", secs * 1000.0);
  return 0;
}
Con esto podemos medir cuántos milisegundos demoró "hacer algo". Todo parece muy bonito hasta que nos damos cuenta de dos grandes problemas:
  1. Tomar una medida única y aislada es igual que tomar un número completamente aleatorio y mostrarlo (no es una muestra representativa). Es mejor repetir las mediciones unas cuantas veces (y hablo del orden de las 100, o 100000, o 1e32 veces), y luego sacar un promedio de todo.
  2. La función clock() no llega a tener una precisión ni de 10 milisegundos (aunque CLOCS_PER_SEC sea 1000 o más).
Una vez dicho esto, el código de arriba no sirve ni para limpiarse los trastes... Así que tenemos que buscar una función con mayor precisión, y además, promediar varias muestras.

Existen otras alternativas como la función gettimeofday, pero bajo Windows sufre del mismo problema de precisión que clock(). Igualmente en Linux funciona perfectamente, así que vale la pena tener en cuenta este código:
#include <stdio.h>
#include <time.h>
#include <sys/time.h>

/* retorna "a - b" en segundos */
double timeval_diff(struct timeval *a, struct timeval *b)
{
  return
    (double)(a->tv_sec + (double)a->tv_usec/1000000) -
    (double)(b->tv_sec + (double)b->tv_usec/1000000);
}

int main(int argc, char *argv[])
{
  struct timeval t_ini, t_fin;
  double secs;

  gettimeofday(&t_ini, NULL);
  /* ...hacer algo... */
  gettimeofday(&t_fin, NULL);

  secs = timeval_diff(&t_fin, &t_ini);
  printf("%.16g milliseconds\n", secs * 1000.0);
  return 0;
}
Como puede ver la estructura timeval contiene dos campos, segundos y microsegundos transcurridos (tv_sec y tv_usec respectivamente), por lo tanto ofrece una precisión de microsegundos. De todas formas, como decía esto en Windows no sirve y la razón es sencilla, en la misma MSDN explican que el temporizador del sistema corre aproximadamente a unos 10 milisegundos, por lo tanto, cualquier función que lo utilice nos estará dando la misma asquerosa precisión (inclusive al utilizar GetSystemTimeAsFileTime y FILETIME). Por lo tanto la solución es utilizar lo que se conoce en el mundo de Windows como el "contador de rendimiento de alta resolución" (high-resolution performance counter):
#include <stdio.h>
#include <windows.h>

/* retorna "a - b" en segundos */
double performancecounter_diff(LARGE_INTEGER *a, LARGE_INTEGER *b)
{
  LARGE_INTEGER freq;
  QueryPerformanceFrequency(&freq);
  return (double)(a->QuadPart - b->QuadPart) / (double)freq.QuadPart;
}

int main(int argc, char *argv[])
{
  LARGE_INTEGER t_ini, t_fin;
  double secs;

  QueryPerformanceCounter(&t_ini);
  /* ...hacer algo... */
  QueryPerformanceCounter(&t_fin);

  secs = performancecounter_diff(&t_fin, &t_ini);
  printf("%.16g milliseconds\n", secs * 1000.0);
  return 0;
}
En este caso, imagine que QueryPerformanceCounter es como clock() y QueryPerformanceFrequency es como CLOCKS_PER_SEC. Es decir, la primera función nos da el valor del contador, y la segunda su frecuencia (en ciclos por segundo, hertz). Cabe aclarar que un LARGE_INTEGER es una forma de representar un entero de 64 bits por medio de una unión (union).

Como tarea al lector, si es que existe alguno, le queda hacer una versión "portable" (entre Windows y Linux) para medir el rendimiento (con unos cuantos #ifdef WIN32 y #endif sería suficiente).

18 de Marzo del 2008: acá transcribo una macro que me pasó el amigo Carlos Becker para medir el tiempo de una rutina en Linux mediante clock_gettime:
#define TIME_THIS(X) \
  { \
    struct timespec ts1, ts2; \
    clock_gettime( CLOCK_REALTIME, &ts1 ); \
    X; \
    clock_gettime( CLOCK_REALTIME, &ts2 ); \
    printf( #X " demora: %f\n", \
      (float) ( 1.0*(1.0*ts2.tv_nsec - ts1.tv_nsec*1.0)*1e-9 \
      + 1.0*ts2.tv_sec - 1.0*ts1.tv_sec ) ); \
  }

/* podemos usarla así */
{
  double x, y, z;
  x = 2.0;
  y = 4.0;
  TIME_THIS(z = sqrt(x*x + y*y));
}
Lo que da como resultado:
z = sqrt(x*x + y*y) demora: 0.015164

domingo, 17 de febrero de 2008

Conseguir un compilador

Editado 21 de mayo 2011: Este post se encuentra aquí sólo a fines históricos, ahora usted puede usar el instalador automático de MinGW.

Editado 9 de septiembre 2010: Aunque este post contiene información útil de cómo instalar MinGW con gcc 3.4, tal vez prefiera antes probar este nuevo post sobre cómo descargar MinGW con gcc 4.5 de forma automática.

En este blog vamos a hablar sobre los lenguajes de programación C y C++, y cómo usar software GNU para hacer aplicaciones. También voy a tratar de investigar algo de ensamblador.

De todas formas, para evitar hacer un primer post completamente inútil "de presentación", voy a pasar a explicarles cómo pueden instalar un compilador gratuito en Windows desde cero, y hacer un pequeño programa "Hola Mundo" sin pensar mucho (eso sí, van a tener que leer un poco). El proceso para Linux es más sencillo, ya que el compilador suele venir instalado por defecto en la mayoría de las distribuciones.

Primero, algunas definiciones importantes:

  • MinGW: Un conjunto de programas y bibliotecas que permiten crear aplicaciones para Windows.
  • GCC: El compilador. Transforma código escrito en C/C++ a un archivo ejecutable (.exe). En realidad hace miles de maravillas más, pero por ahora nos vamos a quedar con esta definición.
  • MSYS: Es un conjunto mínimo de programas para poder ejecutar scripts "a la" Unix. Además incluye una terminal que emula un pequeño entorno GNU/Linux.
  • Emacs: La madre de los editores de texto. Alguna vez dije que era una porquería o muy complicado, y que conviene usar editores como el RHIDE o Setedit. Bueno, la cosa es que con "un poco" de práctica uno comprende el poder de esta herramienta.
  • make: Un programa que puede compilar muchos archivos y linkearlos en un ejecutable de forma automatizada (siempre y cuando le escribamos un archivo con las órdenes adecuadas). Generalmente los programadores están acostumbrados a utilizar un IDE (como el DevC++) que maneje el proyecto, calcule las dependencias entre archivos, compile todo lo necesario, linkee, ejecute y depure. Si bien esto es cómodo, espero poder enseñarles a automatizar la compilación y liberarlos de un determinado IDE para compilar los programas.
  • cmd.exe: Es la línea de comandos de Windows. Si nunca lo utilizó, es hora que se ponga a usarlo. Para iniciar la línea de comandos debe ir al menú "Inicio/Ejecutar..." e introducir "cmd". ¿Cómo se usa? Bueno, empiece a buscar...
Ahora nos dedicaremos a instalar el MinGW, para eso debe comenzar a bajarse los siguientes archivos (si no sabe cómo descomprimir un archivo .tar.gz...):
Una vez que consiga todos estos archivos, debe crear una carpeta C:\MinGW (le recomiendo esa ubicación) y descomprimir todo ahí mismo. Para instalar el depurador (gdb) deberá ejecutar el instalador, seguir los pasos, y colocar como carpeta de instalación "C:\MinGW".

Una vez instalado todo, usted ya está en condiciones para compilar código en C o C++. Antes verifique que en dicha carpeta tiene una estructura de directorios como la siguiente:

C:\MinGW
  bin\
  doc\
  include\
  info\
  lib\
  libexec\
  man\
  mingw32\
Si así es, significa que todo salió bien (bueno, tampoco descomprimir unos archivos es algo tan complicado).

Para estar seguros que el compilador funcione, vamos a crear el programa de prueba por excelencia, el "Hola Mundo". Abra el bloc de notas (Inicio/Ejecutar/notepad) y escriba en un archivo el siguiente código:

#include <stdio.h>

int main(int argc, char *argv[])
{
  printf("Hola Mundo\n");
  return 0;
}
Guarde el archivo como "C:\MinGW\hola.c". Ahora abra la línea de comandos cmd.exe, y ahí mismo ejecute los siguientes comandos:
cd C:\MinGW
set PATH=C:\MinGW\bin;%PATH%
gcc hola.c
Verá que el archivo "a.exe" se crea en el directorio C:\MinGW. Si ejecuta el programa a.exe visualizará en la salida de la consola el mensaje más estúpido jamás escrito: "Hola Mundo"
C:\MinGW>a.exe
Hola Mundo
A continuación le paso a explicar qué hicimos en esta ráfaga de pasos:
  • "cd C:\MinGW" es para ubicarnos en el directorio donde se encuentra el archivo o proyecto que queremos compilar (si no sabía esto, es porque todavía no estuvo buscando cómo usar cmd.exe...).
  • "set PATH=C:\MinGW\bin;%PATH%" es un conjunto de varias cosas: "PATH" es una variable de entorno que indica cuáles son los directorios donde podemos encontrar archivos ejecutables (archivos ejecutables como gcc.exe, cpp.exe, etc.); "set" es un comando del shell para cambiar el valor de una variable de entorno; y "%PATH%" devuelve el valor de dicha variable de entorno. Por lo tanto, lo único que estamos haciendo aquí es agregar una nueva ruta (C:\MinGW\bin) a PATH para que el shell encuentre los archivos ejecutables que a nosotros nos interesa (el preprocesador, compilador, etc.). Si desea ver dónde busca el shell los archivos ejecutables actualmente, puede usar el siguiente comando: "echo %PATH%"
  • "gcc hola.c" compila el programa "hola.c" y deja el resultado (linkea o linkedita) en un archivo llamado "a.exe" (o "a.out" en Linux). En otros posts veremos cómo podemos especificar un nombre distinto a este archivo de salida.
De todo lo visto, lo más molesto es la configuración de la variable de entorno, es decir, si pensamos compilar varias veces, cada vez que abramos un shell (cmd.exe) deberemos hacer un "set PATH=C:\MinGW\bin;%PATH%". Para evitar este paso tenemos dos opciones:
  1. Si tiene accesos de Administrador, puede hacer click derecho sobre "Mi PC", "Propiedades", "Opciones Avanzadas", "Variables de Entorno" y modificar la variable PATH agregándole el valor "C:\MinGW\bin" (recuerde no tocar las demás rutas, además de agregar un ";" para separar las distintas rutas entre sí).
  2. En otro caso, puede crearse un archivo de comandos por lotes mi-mingw.bat para iniciar el shell adecuadamente con el PATH ya configurado. Por ejemplo, un mi-mingw.bat posible podría ser:
    set PATH=C:\MinGW\bin;%PATH%
    start cmd.exe
    
    Este archivo podemos dejarlo en el Escritorio y cada vez que tengamos ganas de compilar, con un simple doble click ya podemos empezar. Inclusive, en vez de iniciar "start cmd.exe" podríamos iniciar algún IDE o editor de texto como Emacs. Pero eso para otro post.
Bueno, por hoy me cansé. Cualquier problema o pregunta que tengan, los comentarios están abiertos (y bien moderados :)