Optimitzar binaris al C/C++ amb GCC i Clang

Darrera actualització: 14/01/2026
Autor: Isaac
  • La base d'una bona optimització a C/C++ és combinar sensatament -march, nivells -O i algunes opcions segures com -pipe.
  • Tècniques avançades com LTO, PGO, OpenMP o Graphite poden donar grans millores, però augmenten la complexitat de compilació i depuració.
  • Les flags de hardening (FORTIFY, stack protector, PIE, relro/now) reforcen la seguretat a canvi de certa pèrdua de rendiment.
  • CMake i els diferents generadors permeten mantenir un codi portable entre GCC, Clang, MSVC i diferents plataformes sense tocar el codi font.

Optimitzar binaris C C++ amb GCC i Clang

Quan comences a jugar amb les opcions de compilació en C i C++ és fàcil caure a la temptació d'activar totes les flags “molones” que veus per internet. Però la realitat és que una mala combinació de paràmetres pot fer el teu sistema inestable, trencar compilacions o, encara pitjor, generar binaris que fallen de formes molt subtils o que requereixen extreure'n informació; en aquests casos pot ser útil extreure text ocult en binaris per investigar.

L'objectiu d'aquesta guia és que entenguis, de forma pràctica i sense embuts, com optimitzar binaris al C/C++ amb GCC i Clang usant les opcions correctes: des de les clàssiques -O2, -march y -pipe, fins a tècniques avançades com LTO, PGO, OpenMP, Graphite o hardening de seguretat. Veureu també com encaixa tot això amb CMake, MinGW/MSYS2, Visual Studio, Xcode o Ninja per muntar un entorn portàtil i mantenible.

Què són CFLAGS, CXXFLAGS i com fer-los servir sense embolicar-la

En gairebé tots els sistemes tipus Unix (Linux, BSD, etc.) s'utilitzen les variables CFLAGS y CXXFLAGS per passar opcions al compilador de C i C++. No formen part de cap estàndard formal, però són tan habituals que qualsevol sistema de build ben escrit (Make, Autotools, CMake, Meson…) les respecta.

En distribucions com Gentoo aquestes variables es defineixen de forma global a /etc/portage/make.conf, ia partir d'aquí s'hereten tots els paquets que es compilen amb Portage. En altres sistemes les pots exportar a l'intèrpret d'ordres o posar-les en un Makefile, un script de CMake o similars.

És força comú definir CXXFLAGS reutilitzant el contingut de CFLAGS i, si cal, afegir alguna opció específica per a C++. Per exemple: CXXFLAGS="${CFLAGS} -fno-exceptions". L'important és no ficar-hi flags de forma indiscriminada, perquè s'aplicaran a tot allò que compileixes.

Convé tenir clar que les opcions agressives a CFLAGS/CXXFLAGS poden trencar compilacions, introduir bugs molt difícils de depurar o fins i tot alentir els binaris. Els nivells alts d'optimització no sempre donen més rendiment, i algunes transformacions poden explotar supòsits que el vostre codi no compleix.

Optimització bàsica: -march, -mtune i nivells -O

La base de qualsevol ajustament assenyat passa per tres peces: seleccionar l'arquitectura de CPU, triar el nivell d'optimització i, de vegades, activar petites millores inofensives com a -pipe. Gairebé tota la resta hauria de venir després i amb cap.

Triar arquitectura: -march, -mtune i companyia

L'opció -march=<cpu> indica a GCC/Clang per a quina família concreta de processador va a generar codi. Permet utilitzar instruccions específiques (SSE, AVX, AVX2, AVX-512, etc.) i ajustar detalls de l'ABI. Si et passes de llest i tries una CPU massa moderna, el binari simplement no arrencarà en màquines antigues.

Per saber què suporta el teu processador, a Linux pots consultar /proc/cpuinfo o fer servir ordres del propi compilador de l'estil gcc -Q -O2 --help=target. A x86-64 modern s'han estandarditzat perfils genèrics com x86-64-v2, x86-64-v3 y x86-64-v4, que agrupen conjunts d'instruccions creixents i estan suportats des de GCC 11.

A més -march, hi ha -mtune=<cpu> per “afinar” la planificació del codi cap a un model concret sense utilitzar instruccions noves. A arquitectures no x86 també apareixen -mcpu y -mtune com a opcions rellevants (ARM, PowerPC, SPARC…). A x86, -mcpu està de fet obsolet.

Un truc força usat és -march=native, que fa que el compilador detecti la CPU de la màquina local i activi automàticament les extensions apropiades. Això ve de cinema en entorns on només executaràs els binaris a la mateixa màquina on els compiles, però és un parany mortal si generes paquets per a altres CPUs.

En processadors recents de Intel i AMD, GCC incorpora noms específics per a cada família, com -march=rocketlake, -march=sapphirerapids, -march=znver2 o -march=znver3. Aquestes opcions agrupen les instruccions avançades (AVX2, AVX-512, FMA, etc.) de cada generació i permeten esprémer força el maquinari quan saps on desplegaràs.

Nivells d'optimització -O: quan fer servir cadascun

L'opció -O controla el nivell global d'optimització aplicat al codi. Cada esglaó activa un conjunt de transformacions més ampli, amb impacte tant en temps de compilació com a consum de memòria i facilitat de depuració.

  • -O0: sense optimitzar. És l'opció per defecte si no indiqueu res. Compila ràpid i genera codi molt fàcil de depurar, però lent i gran. Ideal per a desenvolupament inicial i per investigar bugs complicats.
  • -O1: primer nivell d'optimització. Aplica millores relativament barates que solen fer un salt decent en rendiment sense fer la compilació massa pesada.
  • -O2: és el nivell recomanat per a ús general a la majoria de projectes. Equilibra bé rendiment, temps de compilació i estabilitat, i per això és el valor que moltes distribucions usen per defecte.
  • -O3: activa totes les optimitzacions de -O2 més transformacions addicionals agressives, com desenrotllament de bucles molt fort o vectorització més intensa. En alguns codis numèrics pot anar de luxe, però també és més propens a destapar UB al codi oa inflar la mida de l'executable.
  • -Os: intenta reduir la mida del binari prioritzant l'espai sobre la velocitat. És útil en entorns amb emmagatzematge o memòria cau molt limitats.
  • -Oz (GCC 12+): porta l'estalvi de mida a l'extrem, accepta baixades notables de rendiment. Útil per a binaris molt petits o escenaris molt específics.
  • -Ofast: és com un -O3 sense respecte estricte als estàndards de C/C++. Permet trencar algunes garanties del llenguatge per treure'n rendiment extra, especialment en càlculs de coma flotant. Cal fer-lo servir sabent perfectament què es fa.
  • -Og: pensat per depurar. Aplica només les optimitzacions que no interfereixen gaire amb el debugger i deixa el codi en un punt mitjà entre -O0 y -O1.

Nivells per sobre de -O3 com a -O4 o -O9 són pur fum: el compilador els accepta però internament els tracta com -O3. No hi ha màgia oculta allà, només postureig.

  Valve allibera el SDK de Team Fortress 2 i revoluciona la comunitat de mods

Si comences a veure compilacions que fallen misteriosament, penges rars o resultats diferents segons l'optimitzador, un bon pas de diagnòstic és baixar temporalment a -O1 o fins i tot -O0 -g2 -ggdb per obtenir binaris fàcilment depurables i reportar el bug amb informació útil.

-pipe i altres opcions bàsiques

La flag -pipe indica al compilador que faci servir canonades en memòria en lloc de fitxers temporals en disc entre fases de compilació (preprocés, compilació, acoblament). Normalment fa el procés una mica més ràpid, encara que consumeix més RAM. En màquines amb molt poca memòria pot acabar provocant que el sistema mati el compilador, així que utilitzi'l amb mesura en aquests casos.

Altres opcions tradicionals com -fomit-frame-pointer permeten alliberar el registre del punter de pila per a general més codi, però dificulten la depuració amb backtraces nets. En arquitectures modernes x86-64 el compilador ja gestiona això força bé i sovint ni tan sols cal posar-la a mà.

Extensions SIMD, Graphite i vectorització de bucles

Els compiladors moderns per a x86-64 activen automàticament moltes instruccions SIMD en funció de la CPU escollida amb -march. Tot i així, veuràs per aquí flags com -msse2, -mavx2 o similars que es poden afegir de manera explícita.

En general, si utilitzeu un -march adequat no necessites activar manualment -msse, -msse2, -msse3, -mmmx o -m3dnow, perquè ja vénen implícites. Només té sentit forçar-les en CPU molt particulars on GCC/Clang no les activin per defecte.

Per a bucles complexos, GCC inclou el conjunt d'optimitzacions grafit, que es recolzen a la llibreria ISL. Mitjançant flags com -ftree-loop-linear, -floop-strip-mine y -floop-block el compilador analitza bucles i els pot reestructurar per millorar la localitat de dades i la paral·lelització; en casos concrets, veure exemples de C a baix nivell ajuda a adaptar el codi per a aquestes transformacions.

Aquestes transformacions poden donar bons resultats en codi numèric pesat, però no són innòcues: poden disparar el consum de RAM en compilar i provocar errors en projectes grans que no estaven escrits pensant-hi. Per això es recomana activar-les només en peces de codi concretes o projectes on s'hagi provat que funcionen bé.

Paral·lelisme: OpenMP, -fopenmp i -ftree-parallelize-loops

Si el teu codi utilitza OpenMP, tant GCC com Clang ofereixen suport força sòlid mitjançant l'opció -fopenmp. Això permet paral·lelitzar seccions de codi, sobretot bucles, mitjançant directives al propi codi font, i que el compilador generi el treball en diversos fils.

A més -fopenmp, GCC inclou l'opció -ftree-parallelize-loops=N on N sol establir-se al nombre de nuclis disponibles (per exemple usant $(nproc) en scripts de build). Això intenta paral·lelitzar bucles automàticament sense necessitat d'afegir directives manuals, encara que l'èxit depèn molt de la manera com estigui escrit el codi.

  Guia completa per obrir i utilitzar el gestor de tasques a Chromebook

Cal tenir en compte que activar OpenMP globalment en tot un sistema pot ser molt problemàtic. Hi ha projectes que no estan preparats per fer-ho, altres que usen els seus propis models de concurrència i alguns que directament fallen en compilar quan veuen -fopenmp. El que és sensat és habilitar-ho per projecte o fins i tot per mòdul, no a les CFLAGS globals del sistema.

Optimització en temps d'enllaç: LTO

La Link Time Optimization (LTO) permet que el compilador no es quedi limitat a un únic fitxer font a l'hora d'optimitzar, sinó que vegeu el programa sencer a la fase d'enllaç i apliqui optimitzacions globals sobre tots els objectes implicats.

A GCC s'activa amb -flto, i es pot indicar un nombre de fils, per exemple -flto=4, o deixar que detecti el nombre de nuclis amb -flto=auto. Si a més s'usa -fuse-linker-plugin juntament amb l'enllaçador or i el plugin LTO instal·lat als binutils, el compilador pot extreure informació LTO fins i tot de llibreries estàtiques que participin a l'enllaç.

LTO sol generar executables una mica més petits i, en molts casos, més ràpids, perquè elimina codi mort i pot inlining entre mòduls. A canvi, el temps de compilació i el consum de memòria es disparen, especialment en projectes grans amb milers de fitxers objecte.

En entorns com Gentoo, on es recompila tot el sistema des de fonts, aplicar LTO de forma global continua considerant-se una mica delicat: hi ha molts paquets que encara no es porten bé amb LTO i requereixen desactivar-la de manera selectiva. Per això sol recomanar activar-la només en projectes concrets o en compilacions de GCC/Clang on realment es noti el benefici.

PGO: Profile Guided Optimization

La optimització guiada per perfils (PGO) consisteix a compilar el programa una primera vegada amb instrumentació, executar-lo amb càrregues de treball representatives per recopilar estadístiques d'execució i, després, recompilar-lo fent servir aquests perfils per guiar l'optimitzador.

A GCC el flux típic és: primer compilar amb -fprofile-generate, executar el programa (o els seus tests) per generar dades de perfil, i després recompilar amb -fprofile-use apuntant al directori on s'han emmagatzemat els fitxers de perfil. Amb opcions addicionals com -fprofile-correction o desactivant certs avisos (-Wno-error=coverage-mismatch) es poden evitar errors freqüents derivats de canvis en el codi entre una fase i una altra; a més sol ser útil monitoritzar rendiment amb eBPF i perf per obtenir perfils precisos.

Bé aplicada, PGO pot proporcionar millores de rendiment molt superiors a les de simplement pujar el nivell de -Operquè pren decisions basades en dades reals d'execució, no en models genèrics. El problema és que és un procés pesant: cal repetir-lo a cada actualització rellevant del codi, i depèn molt que l'escenari de prova sigui representatiu de l'ús real.

Alguns projectes (inclòs el propi GCC en certes distribucions) ofereixen ja flags o scripts específics per activar PGO de forma automatitzada, però en general continua sent una tècnica per a usuaris avançats que estiguin disposats a invertir temps en el procés.

Hardening: seguretat a base de flags

Més enllà de la velocitat, molts entorns posen el focus a endurir els binaris davant de vulnerabilitats, fins i tot a costa de perdre una mica de rendiment. GCC i els enllaçadors actuals ofereixen un bon ventall de opcions de hardening que es poden activar des de CFLAGS/CXXFLAGS i LDFLAGS.

Algunes de les més habituals són:

  • -D_FORTIFY_SOURCE=2 o =3: afegeix comprovacions addicionals en certes funcions de la libc per detectar desbordaments de memòria intermèdia en temps d'execució.
  • -D_GLIBCXX_ASSERTIONS: activa comprovacions de límits en contenidors i cadenes de C++ de la STL, detectant accessos fora de rang.
  • -fstack-protector-strong: insereix canaris a la pila per detectar escriptures que la corrompin.
  • -fstack-clash-protection: mitiga atacs basats en xocs entre pila i altres regions de memòria.
  • -fcf-protection: afegeix proteccions de flux de control (per exemple, contra atacs tipus ROP) en arquitectures que ho suporten.
  • -fpie juntament amb -Wl,-pie: genera executables posicionables, necessaris per a un ASLR efectiu.
  • -Wl,-z,relro y -Wl,-z,now: endureixen la taula de relocacions i desactiven el binding mandrós de símbols, dificultant certs vectors d'atac.
  Fusion 360 vs Solid Edge vs CATIA: Quin és el millor programari CAD per a tu?

Perfils hardened d'algunes distribucions ja porten moltes d'aquestes opcions activades per defecte. Activar-les a mà sense entendre l'impacte pot portar-te a binaris notablement més lents, sobretot en aplicacions grans o molt intensives en memòria, però en servidors exposats o escriptoris sensibles sol ser un preu raonable.

Escollir compilador i entorn: GCC, Clang, MSVC, MinGW, Xcode…

A la pràctica, moltes vegades no només tries flags, sinó quin compilador i quin toolchain complet utilitzaràs a cada plataforma. GCC i Clang solen estar molt semblants en rendiment, i les diferències són més visibles en diagnòstics, temps de compilació o compatibilitat amb determinades extensions.

En Windows tens diverses rutes: Visual Studio (MSVC) amb els seus toolsets v143, v142, etc.; o bé MinGW-w64 a través de MSYS2 que et dóna GCC i Clang nadius de Windows juntament amb les llibreries Win32 necessàries. MSYS2 es gestiona amb pacman i ofereix entorns MinGW64 (basat amb MSVCRT clàssica) i UCRT64 (amb Universal CRT, més modern).

A macOS el camí estàndard és Xcode amb clang/clang++, on el concepte clau és el Base SDK (la versió del sistema per a la qual es compila) i el Objectiu de desplegament (la versió mínima de macOS en què vols que la teva app funcioni). Ajustar correctament aquest parell evita el clàssic desastre de compilar només per a la darrera versió del sistema i que els teus binaris no arrenquin en versions una mica més antigues.

A Linux el normal és estirar GCC i Make o Ninja, potser amb CMake com a metagenerador. A més, distribucions com Ubuntu permeten instal·lar múltiples versions de GCC i seleccionar-les amb update-alternatives, de manera semblant a com en macOS uses xcode-select per canviar de Xcode.

Si necessites entorns de depuració còmodes sobre projectes generats amb Make o Ninja (que són monoconfiguració), Eclipsi CDT y Codi de Visual Studio són dues opcions molt arreglades: CMake pot produir els fitxers de projecte que necessiten o integrar-s'hi directament per configurar, compilar i depurar.

Portabilitat i CMake: mateix codi, diferents toolchains

Aconseguir que un projecte a C/C++ compile sense tocar codi a Windows, Linux i macOS exigeix ​​combinar bé CMake, els generadors disponibles i els diferents compiladors. La idea és que larxiu CMakeLists.txt descriviu el projecte de forma abstracta i CMake generi el tipus de projecte adequat a cada plataforma.

A Windows pots invocar CMake amb -G "Visual Studio 17 2022" per produir una solució amb msbuild, o amb -G "Ninja" per tenir builds més ràpids des de consola. A més, mitjançant -T v143, v142, etc., selecciones el Platform Toolset (versió del compilador MSVC) i amb -A x64, Win32 o arm64 tries larquitectura.

Amb MinGW/MSYS2 el més normal és fer servir -G "MinGW Makefiles" o -G "Ninja" i, a través de les variables CMAKE_C_COMPILER y CMAKE_CXX_COMPILER, triar si vols GCC o Clang. En aquest cas les configuracions (Debug, Release, etc.) es controlen via -DCMAKE_BUILD_TYPE, ja que Make i Ninja són monoconfiguració.

A macOS, -G Xcode et dóna un projecte perfecte per depurar a l'IDE, i pots controlar el SDK i el Deployment Target amb variables com CMAKE_OSX_DEPLOYMENT_TARGET. Si només vols Make o Ninja, fas servir els mateixos generadors que a Linux.

La gràcia de tot això és que, ben muntat, pots mantenir una sola base de codi i un conjunt coherent de flags (de vegades ajustats per plataforma) i compilar en qualsevol dels entorns sense haver de caminar tocant el codi font cada dos per tres. Això sí, convé no oblidar la màxima: primer que funcioni bé, després ja premem l'accelerador de l'optimització.

Amb tot el que s'ha vist, la idea general és quedar-se amb una combinació moderada però efectiva (una mica de l'estil -O2 -march=<cpu adecuada> -pipe més algun hardening raonable) i reservar les bales grasses —LTO, PGO, Graphite, OpenMP agressiu— per a aquells projectes o mòduls on realment estan mesurades les millores i s'accepten els costos de manteniment i depuració que porten sota el braç.

Monitoritzar rendiment amb eBPF i bpftrace
Article relacionat:
Monitoritzar rendiment amb eBPF, bpftrace i perf a Linux