Minimalistické překladače jazyka C: tcc a Chibicc

21. 3. 2024
Doba čtení: 33 minut

Sdílet

 Autor: Depositphotos
Mezi nejčastěji používané překladače céčka na Linuxu patří GCC a Clang. Ovšem kromě těchto známých překladačů můžeme použít i takzvané „malé“ překladače, mezi které patří zejména Tiny C Compiler a taktéž Chibicc.

Obsah

1. Minimalistické překladače jazyka C: tcc a Chibicc

2. Programovací jazyk C

3. Překladače programovacího jazyka C v Linuxu

4. Tiny C Compiler

5. Získání, konfigurace a překlad Tiny C Compileru

6. Překladač céčka Chibicc

7. Překlad překladače (sic) Chibicc

8. Programy použité pro porovnání Tiny C Compileru a překladače Chibicc s GCC

9. Testujeme rychlost překladu

10. Zhodnocení rychlosti překladu

11. Vliv (absence) optimalizací na rychlost výsledných programů

12. Rychlost vykreslení fraktálu

13. Rychlost vyřešení Sudoku

14. Tiny C Compiler ve funkci „interpretru“

15. Hlášení o chybách a varování

16. Zdrojový kód s množstvím potenciálních problémů

17. Chybová hlášení a varování vypisovaná překladači céčka: GCC a Clang

18. Chybová hlášení a varování vypisovaná tcc a Chibiccem

19. Kde tedy „malé“ překladače použít?

20. Odkazy na Internetu

1. Minimalistické překladače jazyka C: tcc a Chibicc

Když vývojář používající operační systém Linux (nebo abychom neurazili, tak GNU/Linux) použije sousloví „překladač céčka“, je prakticky stoprocentně jisté, že má na mysli překladač programovacího jazyka C patřící do rodiny GCC (GNU Compiler Collection) nebo Clang (postavený nad LLVM). Ovšem ve skutečnosti existuje pro Linux mnohem větší množství překladačů programovacího jazyka C. Mezi ně patří například minimalisticky pojatý projekt Tiny C Compiler, jehož největší předností je velká rychlost překladu i možnost použít tento překladač ve funkci interpretru – je jím tedy možné céčkové programy přímo „spouštět“, podobně jako skripty psané v Perlu či v BASHi, Pythonu, Ruby atd. atd. Ovšem existuje i relativně velké množství dalších překladačů céčka. V dnešním článku se kromě již zmíněného Tiny C Compileru (zkráceně jen tcc) zaměříme i na překladač nazvaný chibicc: A Small C Compiler (zkracovat to budeme na chibicc).

Poznámka: jak tcc tak i Chibicc do značné míry (či zcela) splňují standardy jazyka C. Existují ovšem i další „malé“ překladače či interpretry jazyka C, které se vlastně jazyku C jen přibližují, ale žádný standard plně nesplňují. Těmito projekty se dnes nebudeme zabývat.

2. Programovací jazyk C

Jedním z nejpopulárnějších v současnosti používaných programovacích jazyků je stále (což je mimochodem zajímavé a hodně to vypovídá o realitě v IT) programovací jazyk C navržený Dennisem Ritchiem. Jazyk C se postupně vyvinul z programovacích jazyků BPCL (autor Martin Richards, 1966) a B (autor Ken Thompson, 1970) až do současné podoby, která byla standardizována v několika normách, z nichž nejznámější je pravděpodobně stále ještě ISO/IEC 9899:1999 známá pod zkráceným označením C99 (následoval C11 neboli ISO/IEC 9899:2011, C17 neboli ISO/IEC 9899:2018 a konečně C23 neboli ISO/IEC 9899:2024 ). Starší, dodnes v některých případech stále ještě používaný standard se jmenuje ISO 9899:1990, tento starší standard je prakticky shodný (až na jiné číslování jednotlivých paragrafů) s normou ANSI C (ANSI X3.159–1989 „Programming Language C“) a zkráceně se označuje C89 či méně často C90. Se všemi zkratkami C89, C90, C99, C11, C17 a C23 se ještě v tomto článku setkáme.

Programovací jazyk C je i přes absenci některých důležitých vlastností (například mu chybí automatický správce paměti či podpora silného typování a práce s objekty, výjimkami, uzávěry atd.) využívaný jak pro tvorbu open source aplikací, tak i v čistě komerční oblasti – nejedná se jen o vývoj aplikací pro desktopy a servery, ale i pro mikrořadiče či digitální signálové procesory (DSP). Céčko je mnohdy využíváno i ve funkci cílového jazyka, do něhož se překládají (transpilují) programy zapsané v některých vyšších programovacích jazycích – vývojáři, kteří překladače těchto jazyků vytváří, se tak nemusí starat například o nízkoúrovňové optimalizace, protože je za ně již naprogramovali vývojáři překladače céčka. Z historického hlediska je zajímavé, že právě tímto způsobem vznikla první verze jazyka C++ (nástroj Cpre), i když moderní překladače C++ jsou již řešeny odděleně.

To však není zdaleka vše, protože programovací jazyk C je dodnes důležitý i z toho důvodu, že jak rozhraní jader některých operačních systémů (Linux, Microsoft Windows i dalších systémů), tak rozhraní systémových knihoven bylo navrženo s ohledem na jmenné konvence céčka i s ohledem na možnosti jeho linkeru (a ostatní jazyky toto rozhraní s většími či menšími problémy dodržují).

3. Překladače programovacího jazyka C v Linuxu

Vývoj operačního systému GNU/Linux byl do značné míry ovlivněn, a možná bych si dokonce dovolil říci, že vůbec umožněn tím, že na něj byly portovány nástroje označované souhrnným názvem GNU Compiler Collection neboli zkráceně GCC. Tyto nástroje obsahují překladače několika programovacích jazyků, především pak překladače jazyků C, C++, Objective C, Objective C++, Java, Ada a Fortran (později byly přidány překladače pro Go a nedávno i pro Rust, ovšem prozatím ve fázi vývoje). Kromě překladačů jsou součástí GCC i další nástroje, zejména preprocesor používaný jazyky C a C++, dále GNU assembler (gas), linker, nástroj sledující pokrytí kódu testy (gcov) atd. Jedná se o velmi kvalitní nástroje, které navíc podporují obrovské množství různých platforem, operačních systémů a procesorových architektur, samozřejmě včetně procesorů řady x86 a x86_64.

Kromě překladače céčka patřícího do skupiny nástrojů GCC (i samotný překladač se jmenuje GCC – GNU C Compiler, což může být poněkud matoucí, je tedy nutné vždy sledovat kontext, ve kterém se o GCC mluví či píše) je možné v operačním systému Linux použít i další překladače. Mezi ně patří například Clang z projektu LLVM, jenž je zajímavý především po technologické stránce a s jehož technologií se seznámíme v některém z navazujících článků. Dále se pak můžeme (či jsme se mohli) setkat s překladači komerčních firem, například s překladačem vytvořeným společností Intel, který v případě některých typů optimalizací překonával GCC a Clang/LLVM. V neposlední řadě je pak možné v Linuxu (a nutno říci, že nejenom v něm) použít překladače nazvané Tiny C Compiler (zkráceně tcc) a Chibicc, jejichž popisem a porovnáním s GCC se budeme zabývat v navazujících kapitolách.

4. Tiny C Compiler

Tiny C Compiler (zkracovat ho budeme na tcc) je překladač programovacího jazyka C, který byl původně vytvořen Fabricem Bellardem a nyní se o jeho další vývoj a portaci na nové platformy stará komunita vývojářů, protože se samozřejmě jedná o open-source projekt (i když je nutno poznamenat, že vývoj v žádném případě není nijak bouřlivý). Tiny C Compiler v sobě kromě vlastního překladače obsahuje i linker, což znamená, že jeden binární program může sloužit jak pro překlad zdrojových textů (včetně preprocesingu) do objektového kódu, tak pro vytvoření výsledného spustitelného binárního programu. Všechny tři zmíněné funkce jsou implementovány v jediném spustitelném souboru, jehož velikost na platformě x86 nepřesahuje tři sta kilobajtů, což je například v porovnání s GCC zcela zanedbatelná velikost (dokonce i pouze GNU assembler je v binární podobě větší než celý tcc).

Tiny C Compiler podporuje standard C89/C90 i velkou část standardu C99, a to do té míry, že úpravy zdrojových kódů určených pro GCC většinou nejsou zapotřebí. Největší devizou překladače tcc je ovšem blesková rychlost překladu, protože vlastní překlad je jednoprůchodový. Na stránkách tohoto projektu se uvádí, že tcc je přibližně osmkrát rychlejší než překladač GCC (s použitím standardních voleb, tj. bez optimalizací), ovšem jak se můžete dozvědět z dalšího textu, může být tcc v extrémním případě rychlejší zhruba čtyřicetkrát! (a v některých jiných případech jen třikrát rychlejší). Na druhou stranu však tcc za většinou ostatních moderních překladačů céčka významně pokulhává v případě optimalizací prováděných při překladu. O tom, do jaké míry se absence většiny optimalizačních metod projeví na demonstračních příkladech (benchmarcích), si řekneme v navazujících kapitolách.

Co to však znamená pro každodenní praxi vývojáře? Podle mého názoru je ideální zkombinovat vlastnosti tcc (doslova blesková rychlost překladu) a GCC či Clang (optimalizace na několika úrovních) takovým způsobem, že pro samotný vývoj se použije překladač tcc a pro tvorbu výsledné binární podoby programu se naopak využije překladač GCC s příslušnými přepínači (-O3 atd.). Tiny C Compiler lze použít především na architekturách x86 a x86_64, i když existují i verze využitelné na procesorech ARM a DSP čipech řady TMS320C67×x. Všechna měření, jejichž výsledky jsou uvedeny v dalších kapitolách, byly provedeny na počítači s architekturou x86–64.

5. Získání, konfigurace a překlad Tiny C Compileru

Některé distribuce Linuxu obsahují balíček s Tiny C Compilerem. Poté je jeho instalace triviální. Příkladem mohou být distribuce postavené na Debianu/Mintu atd., v nichž je možné Tiny C Compiler nainstalovat takto:

$ sudo apt-get install tcc

Samotná instalace vyžaduje pouze minimum místa na disku (alespoň z dnešního pohledu):

The following NEW packages will be installed:
  tcc
0 upgraded, 1 newly installed, 0 to remove and 22 not upgraded.
Need to get 242 kB of archives.
After this operation, 709 kB of additional disk space will be used.
Get:1 http://archive.ubuntu.com/ubuntu focal/universe amd64 tcc amd64 0.9.27-8 [242 kB]
Fetched 242 kB in 1s (215 kB/s)
Selecting previously unselected package tcc.
(Reading database ... 330120 files and directories currently installed.)
Preparing to unpack .../tcc_0.9.27-8_amd64.deb ...
Unpacking tcc (0.9.27-8) ...
Setting up tcc (0.9.27-8) ...
Processing triggers for install-info (6.7.0.dfsg.2-5) ...
Processing triggers for doc-base (0.10.9) ...
Processing 1 added doc-base file...
Processing triggers for man-db (2.9.1-1) ...

Zjistíme, která verze se skutečně nainstalovala:

$ tcc -v
 
tcc version 0.9.27 (x86_64 Linux)

V případě, že není balíček dostupný nebo pokud si budete chtít vyzkoušet jeho nejnovější verzi, můžeme provést překlad a instalaci přímo ze zdrojových kódů. Je to ve skutečnosti velmi snadné a potřebujeme k tomu pouze jiný překladač céčka (nebo samotný tcc, který umí sám sebe přeložit), git a některé další nástroje dostupné na počítači vývojářů.

Nejprve si naklonujeme repositář s tcc:

$ git clone git://repo.or.cz/tinycc.git
 
Cloning into 'tinycc'...
remote: Counting objects: 16864, done.
remote: Compressing objects: 100% (4304/4304), done.
remote: Total 16864 (delta 12447), reused 16819 (delta 12405)
Receiving objects: 100% (16864/16864), 4.77 MiB | 1.34 MiB/s, done.
Resolving deltas: 100% (12447/12447), done.

Následně spustíme skript configure, který zjistí aktuálně použitý systém, architekturu procesoru a překladač, který bude použitý pro překlad. Výsledek se uloží, takže bude možné použít make pro překlad:

$ ./configure 
 
Binary directory    /usr/local/bin
TinyCC directory    /usr/local/lib/tcc
Library directory   /usr/local/lib
Include directory   /usr/local/include
Manual directory    /usr/local/share/man
Info directory      /usr/local/share/info
Doc directory       /usr/local/share/doc
Source path         /tmp/ramdisk/tinycc
Build OS            Linux x86_64
C compiler          gcc (9.4)
Target OS           Linux
CPU                 x86_64
Triplet             x86_64-linux-gnu
Creating config.mak and config.h

Dále již stačí spustit klasický make:

$ make

Po několika sekundách či desítkách sekund by měl překlad doběhnout. Výsledkem bude spustitelný soubor tcc s velikostí nepřesahující 400kB:

$ ls -sh tcc
 
348K tcc

Tento soubor lze ještě zmenšit na cca 320 kB (předchozí verze tcc dokonce na velikosti 100kB):

$ strip tcc
 
$ ls -sh tcc
 
320K tcc

Nakonec ověříme, jakou verzi tcc jsme vlastně přeložili:

$ ./tcc -v
 
tcc version 0.9.28rc 2024-03-13 mob@2b0a663d (x86_64 Linux)

6. Překladač céčka Chibicc

Dalším minimalisticky pojatým překladačem programovacího jazyka C, s nímž se v dnešním článku ve stručnosti seznámíme, se jmenuje Chibicc. Tento překladač, jehož repositář naleznete na adrese https://github.com/rui314/chibicc, na rozdíl od některých „malých“ překladačů či interpretrů céčka (picoc) splňuje prakticky celý standard C11 (a tím pádem i starší standardy, zejména C89/C90 a C99, které se stále používají). Ovšem již na tomto místě je dobré upozornit na to, že vzhledem k tomu, že tento překladač NEpodporuje optimalizace, bude běh výsledných přeložených programů většinou pomalejší, než v případě GCC či Clangu, což si ostatně ukážeme na demonstračních příkladech, které jsou popsány v osmé kapitole.

7. Překlad překladače (sic) Chibicc

Samotný překlad překladače Chibicc je velmi snadný a opět nám k tomu bude postačovat starý dobrý GCC (či již nainstalovaný tcc – ostatně si to můžete ověřit). Nejprve si naklonujeme adresář s tímto projektem:

$ git clone git@github.com:rui314/chibicc.git
 
Cloning into 'chibicc'...
remote: Enumerating objects: 4584, done.
remote: Counting objects: 100% (686/686), done.
remote: Compressing objects: 100% (292/292), done.
remote: Total 4584 (delta 409), reused 664 (delta 394), pack-reused 3898
Receiving objects: 100% (4584/4584), 822.69 KiB | 1.61 MiB/s, done.
Resolving deltas: 100% (3406/3406), done.

Přejdeme do adresáře s naklonovaným repositářem:

$ cd chibicc/

Samotný překladač je realizován v několika zdrojových souborech. Není jich mnoho, pokud si počet a velikost souborů porovnáme se zdrojovými kódy GCC či Clangu:

$ ls -l *.[hc]
 
-rw-rw-r-- 1 ptisnovs ptisnovs  9811 Mar 19 14:26 chibicc.h
-rw-rw-r-- 1 ptisnovs ptisnovs 43920 Mar 19 14:26 codegen.c
-rw-rw-r-- 1 ptisnovs ptisnovs  4564 Mar 19 14:26 hashmap.c
-rw-rw-r-- 1 ptisnovs ptisnovs 18392 Mar 19 14:26 main.c
-rw-rw-r-- 1 ptisnovs ptisnovs 91158 Mar 19 14:26 parse.c
-rw-rw-r-- 1 ptisnovs ptisnovs 33021 Mar 19 14:26 preprocess.c
-rw-rw-r-- 1 ptisnovs ptisnovs   690 Mar 19 14:26 strings.c
-rw-rw-r-- 1 ptisnovs ptisnovs 19317 Mar 19 14:26 tokenize.c
-rw-rw-r-- 1 ptisnovs ptisnovs  7503 Mar 19 14:26 type.c
-rw-rw-r-- 1 ptisnovs ptisnovs  6980 Mar 19 14:26 unicode.c

Překlad provedeme bez nutnosti konfigurace, pouze přímým vyvoláním make:

$ make
 
cc -std=c11 -g -fno-common -Wall -Wno-switch   -c -o strings.o strings.c
cc -std=c11 -g -fno-common -Wall -Wno-switch   -c -o tokenize.o tokenize.c
cc -std=c11 -g -fno-common -Wall -Wno-switch   -c -o hashmap.o hashmap.c
cc -std=c11 -g -fno-common -Wall -Wno-switch   -c -o type.o type.c
cc -std=c11 -g -fno-common -Wall -Wno-switch   -c -o main.o main.c
cc -std=c11 -g -fno-common -Wall -Wno-switch   -c -o parse.o parse.c
cc -std=c11 -g -fno-common -Wall -Wno-switch   -c -o codegen.o codegen.c
cc -std=c11 -g -fno-common -Wall -Wno-switch   -c -o preprocess.o preprocess.c
cc -std=c11 -g -fno-common -Wall -Wno-switch   -c -o unicode.o unicode.c
cc -std=c11 -g -fno-common -Wall -Wno-switch -o chibicc strings.o tokenize.o hashmap.o type.o main.o parse.o codegen.o preprocess.o unicode.o

Pro zajímavost se podívejme na čas čistého překladu celého Chibiccu. Jedná se o prakticky okamžitý výsledek:

$ make clean
 
$ time make
 
cc -std=c11 -g -fno-common -Wall -Wno-switch   -c -o strings.o strings.c
cc -std=c11 -g -fno-common -Wall -Wno-switch   -c -o tokenize.o tokenize.c
cc -std=c11 -g -fno-common -Wall -Wno-switch   -c -o hashmap.o hashmap.c
cc -std=c11 -g -fno-common -Wall -Wno-switch   -c -o type.o type.c
cc -std=c11 -g -fno-common -Wall -Wno-switch   -c -o main.o main.c
cc -std=c11 -g -fno-common -Wall -Wno-switch   -c -o parse.o parse.c
cc -std=c11 -g -fno-common -Wall -Wno-switch   -c -o codegen.o codegen.c
cc -std=c11 -g -fno-common -Wall -Wno-switch   -c -o preprocess.o preprocess.c
cc -std=c11 -g -fno-common -Wall -Wno-switch   -c -o unicode.o unicode.c
cc -std=c11 -g -fno-common -Wall -Wno-switch -o chibicc strings.o tokenize.o hashmap.o type.o main.o parse.o codegen.o preprocess.o unicode.o
 
real    0m0,667s
user    0m0,563s
sys     0m0,098s

Výsledný překladač má velikost nepatrně přesahující 315kB:

$ ls -l chibicc
 
-rwxrwxr-x 1 ptisnovs ptisnovs 315128 Mar 19 14:28 chibicc

Po odstranění „tuku“ se jeho velikost zmenší na 166kB:

$ strip chibicc
 
 
-rwxrwxr-x 1 ptisnovs ptisnovs 166960 Mar 19 18:01 chibicc

Zkusme si nyní překladač spustit:

$ ./chibicc --help
 
chibicc [ -o <path> ] <file>

Mimochodem – nelze použít hlavičkové soubory GCC s intrinsic (SIMD atd.), což je ale do jisté míry pochopitelné:

$ chibicc -I/usr/include -I/usr/include/SDL2/ main.c gfx.c
 
/usr/include/SDL2/SDL_cpuinfo.h:86: #include <immintrin.h>
                                             ^ immintrin.h: cannot open file: No such file or directory

8. Programy použité pro porovnání Tiny C Compileru a překladače Chibicc s GCC

Pro porovnání vlastností Tiny C Compileru, překladače Chibicc a překladače céčka, jenž je součástí skupiny překladačů GCC, jsem zvolil tři diametrálně odlišné zdrojové kódy. Prvním zdrojovým kódem je kód projektu tth (plným jménem TeX to HTML Translator), který je dostupný na adrese http://silas.psfc.mit.edu/tth/tar­s/download.html. Tento projekt je v několika ohledech extrémní, protože je uložen v jediném céčkovém zdrojovém kódu o velikosti 1192 kB, který má celkem 29 068 řádků! (v jediném zdrojovém souboru). Překlad takto velkého souboru je pro překladač (přesněji řečeno pro překladač GCC) skutečně tvrdým oříškem, o čemž se ostatně přesvědčíme v další kapitole.

Druhým testovacím programem je generátor fraktálů, který při svém spuštění velmi intenzivně používá operace s hodnotami s plovoucí řádovou čárkou (floating point). V tomto programu se názorně ukáže, jak dobře (či naopak špatně) provádí překladač různé optimalizace – a to jak optimalizace výpočtů, tak například i rozbalení smyček. Zdrojový kód tohoto programu vypadá následovně:

#include <stdlib.h>
#include <stdio.h>
 
#include "palette_mandmap.h"
 
void calc_mandelbrot(unsigned int width, unsigned int height, unsigned int maxiter, unsigned char palette[][3])
{
    puts("P3");
    printf("%d %d\n", width, height);
    puts("255");
 
    double cy = -1.5;
    int y;
    for (y=0; y<height; y++) {
        double cx = -2.0;
        int x;
        for (x=0; x<width; x++) {
            double zx = 0.0;
            double zy = 0.0;
            unsigned int i = 0;
            while (i < maxiter) {
                double zx2 = zx * zx;
                double zy2 = zy * zy;
                if (zx2 + zy2 > 4.0) {
                    break;
                }
                zy = 2.0 * zx * zy + cy;
                zx = zx2 - zy2 + cx;
                i++;
            }
            unsigned char *color = palette[i % 256];
            unsigned char r = *color++;
            unsigned char g = *color++;
            unsigned char b = *color;
            printf("%d %d %d\n", r, g, b);
            cx += 3.0/width;
        }
        cy += 3.0/height;
    }
}
 
int main(int argc, char **argv)
{
    if (argc < 4) {
        puts("usage: ./mandelbrot width height maxiter");
        return 1;
    }
    int width = atoi(argv[1]);
    int height = atoi(argv[2]);
    int maxiter = atoi(argv[3]);
    calc_mandelbrot(width, height, maxiter, palette);
    return 0;
}

Výše uvedený zdrojový soubor navíc ještě vyžaduje barvovou paletu ve formátu pole 256×3 celočíselných hodnot typu bajt:

/* taken from Fractint */
unsigned char palette[][3] = {
        {255, 255, 255}, {224, 224, 224}, {216, 216, 216}, {208, 208, 208},
        {200, 200, 200}, {192, 192, 192}, {184, 184, 184}, {176, 176, 176},
        {168, 168, 168}, {160, 160, 160}, {152, 152, 152}, {144, 144, 144},
        {136, 136, 136}, {128, 128, 128}, {120, 120, 120}, {112, 112, 112},
        {104, 104, 104},  {96,  96,  96},  {88,  88,  88},  {80,  80,  80},
        {72,   72,  72},  {64,  64,  64},  {56,  56,  56},  {48,  48,  56},
        {40,   40,  56},  {32,  32,  56},  {24,  24,  56},  {16,  16,  56},
        {8,     8,  56}, {000, 000,  60}, {000, 000,  64}, {000, 000,  72},
        {000, 000,  80}, {000, 000,  88}, {000, 000,  96}, {000, 000, 104},
        {000, 000, 108}, {000, 000, 116}, {000, 000, 124}, {000, 000, 132},
        {000, 000, 140}, {000, 000, 148}, {000, 000, 156}, {000, 000, 160},
        {000, 000, 168}, {000, 000, 176}, {000, 000, 184}, {000, 000, 192},
        {000, 000, 200}, {000, 000, 204}, {000, 000, 212}, {000, 000, 220},
        {000, 000, 228}, {000, 000, 236}, {000, 000, 244}, {000, 000, 252},
        {000,   4, 252},   {4,  12, 252},   {8,  20, 252},  {12,  28, 252},
        {16,   36, 252},  {20,  44, 252},  {20,  52, 252},  {24,  60, 252},
        {28,   68, 252},  {32,  76, 252},  {36,  84, 252},  {40,  92, 252},
        {40,  100, 252},  {44, 108, 252},  {48, 116, 252},  {52, 120, 252},
        {56,  128, 252},  {60, 136, 252},  {60, 144, 252},  {64, 152, 252},
        {68,  160, 252},  {72, 168, 252},  {76, 176, 252},  {80, 184, 252},
        {80,  192, 252},  {84, 200, 252},  {88, 208, 252},  {92, 216, 252},
        {96,  224, 252}, {100, 232, 252}, {100, 228, 248},  {96, 224, 244},
        {92,  216, 240},  {88, 212, 236},  {88, 204, 232},  {84, 200, 228},
        {80,  192, 220},  {76, 188, 216},  {76, 180, 212},  {72, 176, 208},
        {68,  168, 204},  {64, 164, 200},  {64, 156, 196},  {60, 152, 188},
        {56,  144, 184},  {52, 140, 180},  {52, 132, 176},  {48, 128, 172},
        {44,  120, 168},  {40, 116, 160},  {40, 108, 156},  {36, 104, 152},
        {32,   96, 148},  {28,  92, 144},  {28,  84, 140},  {24,  80, 136},
        {20,   72, 128},  {16,  68, 124},  {16,  60, 120},  {12,  56, 116},
        {8,    48, 112},   {4,  44, 108}, {000,  36, 100},   {4,  36, 104},
        {12,   40, 108},  {16,  44, 116},  {24,  48, 120},  {28,  52, 128},
        {36,   56, 132},  {40,  60, 140},  {48,  64, 144},  {52,  64, 148},
        {60,   68, 156},  {64,  72, 160},  {72,  76, 168},  {76,  80, 172},
        {84,   84, 180},  {88,  88, 184},  {96,  92, 192}, {104, 100, 192},
        {112, 112, 196}, {124, 120, 200}, {132, 132, 204}, {144, 140, 208},
        {152, 152, 212}, {164, 160, 216}, {172, 172, 220}, {180, 180, 224},
        {192, 192, 228}, {200, 200, 232}, {212, 212, 236}, {220, 220, 240},
        {232, 232, 244}, {240, 240, 248}, {252, 252, 252}, {252, 240, 244},
        {252, 224, 232}, {252, 208, 224}, {252, 192, 212}, {252, 176, 204},
        {252, 160, 192}, {252, 144, 184}, {252, 128, 172}, {252, 112, 164},
        {252,  96, 152}, {252,  80, 144}, {252,  64, 132}, {252,  48, 124},
        {252,  32, 112}, {252,  16, 104}, {252, 000,  92}, {236, 000,  88},
        {228, 000,  88}, {216,   4,  84}, {204,   4,  80}, {192,   8,  76},
        {180,   8,  76}, {168,  12,  72}, {156,  16,  68}, {144,  16,  64},
        {132,  20,  60}, {124,  20,  60}, {112,  24,  56}, {100,  24,  52},
        {88,   28,  48},  {76,  32,  44},  {64,  32,  44},  {52,  36,  40},
        {40,   36,  36},  {28,  40,  32},  {16,  44,  28},  {20,  52,  32},
        {24,   60,  36},  {28,  68,  44},  {32,  76,  48},  {36,  88,  56},
        {40,   96,  60},  {44, 104,  64},  {48, 112,  72},  {52, 120,  76},
        {56,  132,  84},  {48, 136,  84},  {40, 144,  80},  {52, 148,  88},
        {68,  156, 100},  {80, 164, 112},  {96, 168, 124}, {108, 176, 136},
        {124, 184, 144}, {136, 192, 156}, {152, 196, 168}, {164, 204, 180},
        {180, 212, 192}, {192, 220, 200}, {208, 224, 212}, {220, 232, 224},
        {236, 240, 236}, {252, 248, 248}, {252, 252, 252}, {252, 252, 240},
        {252, 252, 228}, {252, 252, 216}, {248, 248, 204}, {248, 248, 192},
        {248, 248, 180}, {248, 248, 164}, {244, 244, 152}, {244, 244, 140},
        {244, 244, 128}, {244, 244, 116}, {240, 240, 104}, {240, 240,  92},
        {240, 240,  76}, {240, 240,  64}, {236, 236,  52}, {236, 236,  40},
        {236, 236,  28}, {236, 236,  16}, {232, 232,   0}, {232, 232,  12},
        {232, 232,  28}, {232, 232,  40}, {236, 236,  56}, {236, 236,  68},
        {236, 236,  84}, {236, 236,  96}, {240, 240, 112}, {240, 240, 124},
        {240, 240, 140}, {244, 244, 152}, {244, 244, 168}, {244, 244, 180},
        {244, 244, 196}, {248, 248, 208}, {248, 248, 224}, {248, 248, 236},
        {252, 252, 252}, {248, 248, 248}, {240, 240, 240}, {232, 232, 232}};

Třetím příkladem je program pro řešení Sudoku. Algoritmus řešení je velmi jednoduchý a naivní (k dispozici jsou mnohem sofistikovanější algoritmy), což však není v tomto případě podstatné, jelikož nás zajímá rychlost překladu a kvalita výsledného kódu. V tomto programu se velmi často používají operace pro práci s prvky dvourozměrných a trojrozměrných polí, takže se zde ukáže, jak dokáže překladač optimalizovat tento typ operací:

/* ------------------------------------------------------------------ */
/* Benchmark na praci s celociselnymi poli:                           */
/* reseni Sudoku metodou brute-force                                  */
/*                                                                    */
/* Autor: Pavel Tisnovsky                                             */
/* ------------------------------------------------------------------ */
 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
#define ODDELOVAC puts("+--------+--------+--------+");
 
 
 
/* vstupni data: */
/* 1-9 znama hodnota */
/* 0   neznama hodnota (prazdne policko) */
const int src[9][9]={
     {5, 6, 9,   0, 0, 7,   2, 1, 8},
     {7, 0, 2,   0, 0, 1,   0, 3, 0},
     {1, 3, 8,   9, 2, 0,   4, 0, 0},

     {0, 0, 7,   2, 0, 9,   6, 5, 0},
     {0, 0, 4,   0, 0, 5,   8, 9, 3},
     {9, 0, 0,   8, 6, 0,   0, 4, 0},

     {8, 0, 0,   6, 1, 0,   3, 0, 0},
     {4, 1, 0,   3, 9, 2,   0, 0, 0},
     {2, 0, 0,   0, 5, 8,   1, 0, 4}
};
 
/* pracovni matice */
int wrk[9][9];
 
/* matice obsahujici povolene hodnoty pro kazde policko */
int povol[9][9][9];
 
/* pomocna matice s minimalni povolenou hodnotou pro kazde policko */
int minim[9][9];
 
 
 
/* ------------------------------------------------------------------ */
/* Snizeni poctu moznych kombinaci zjistenim duplicit ve sloupcich    */
/* ------------------------------------------------------------------ */
void testSloupce0(void)
{
    int radek, sloupec;
    int cifry[9], cifra;
    int i;
    for (sloupec=0; sloupec<9; sloupec++) {
        for (i=0; i<9; i++)
            cifry[i]=0;
        /* projdi vsechny bunky v radku a zjisti zapsana cisla */
        for (radek=0; radek<9; radek++) {
            cifra=src[radek][sloupec];
            if (cifra>0) { /* obsad cifru */
                cifry[cifra-1]=1;
            }
        }
        /* v matici povol zmen cely radek */
        for (radek=0; radek<9; radek++) {
            if (src[radek][sloupec]==0) {
                for (i=0; i<9; i++) {
                    if (cifry[i])
                        povol[radek][sloupec][i]=0;
                }
            }
        }
    }
}
 
 
 
/* ------------------------------------------------------------------ */
/* Snizeni poctu moznych kombinaci zjistenim duplicit na radcich      */
/* ------------------------------------------------------------------ */
void testRadku0(void)
{
    int radek, sloupec;
    int cifry[9], cifra;
    int i;
    for (radek=0; radek<9; radek++) {
        for (i=0; i<9; i++)
            cifry[i]=0;
        /* projdi vsechny bunky ve sloupci a zjisti zapsana cisla */
        for (sloupec=0; sloupec<9; sloupec++) {
            cifra=src[radek][sloupec];
            if (cifra>0) { /* obsad cifru */
                cifry[cifra-1]=1;
            }
        }
        /* v matici povol zmeni cely sloupec */
        for (sloupec=0; sloupec<9; sloupec++) {
            if (src[radek][sloupec]==0) {
                for (i=0; i<9; i++) {
                    if (cifry[i])
                        povol[radek][sloupec][i]=0;
                }
            }
        }
    }
}
 
 
 
/* ------------------------------------------------------------------ */
/* Snizeni poctu moznych kombinaci zjistenim duplicit v blocich 3x3   */
/* ------------------------------------------------------------------ */
void testBloky0(void)
{
    int blokx, bloky;
    int cifry[9], cifra;
    int i, j;
    for (bloky=0; bloky<9; bloky+=3) {
        for (blokx=0; blokx<9; blokx+=3) {
            for (i=0; i<9; i++) /* v kazdem bloku 3x3 nejprve vymazeme seznam cifer */
                cifry[i]=0;
            for (j=0; j<3; j++) { /* projdi blok a zjisti cisla */
                for (i=0; i<3; i++) {
                    cifra=src[j+bloky][i+blokx];
                    if (cifra>0) { /* obsad cifru */
                        cifry[cifra-1]=1;
                    }
                }
            }
            for (j=0; j<3; j++) { /* projdi blok a znuluj nalezene cifry */
                for (i=0; i<3; i++) {
                    if (src[j+bloky][i+blokx]==0) { /* v tomto poli ma smysl menit cifry */
                        int k;
                        for (k=0; k<9; k++) {
                            if (cifry[k])
                                povol[j+bloky][i+blokx][k]=0;
                        }
                    }
                }
            }
        }
    }
}
 
 
 
/* ------------------------------------------------------------------ */
/* Test, zda je ve vsech sloupcich devet ruznych hodnot               */
/* ------------------------------------------------------------------ */
int testSloupce(void)
{
    int radek, sloupec;
    int cifry[9];
    for (sloupec=0; sloupec<9; sloupec++) {
        for (radek = 0; radek<9; radek++) {
            cifry[radek] = 0;
        }
        /* projdu vsechny polozky v radku a zjistim cisla */
        for (radek=0; radek < 9; radek++) {
            /* cifra je uz obsazena? */
            if (cifry[wrk[radek][sloupec]-1] != 0)
                return 0;
            else  /* obsad cifru */
                cifry[wrk[radek][sloupec]-1]=1;
        }
    }
    return 1;
}
 
 
 
/* ------------------------------------------------------------------ */
/* Test, zda je na vsech radcich devet ruznych hodnot                 */
/* ------------------------------------------------------------------ */
int testRadky(void)
{
    int radek, sloupec;
    int cifry[9];
    for (radek=0; radek<9; radek++) {
        for (sloupec=0; sloupec<9; sloupec++) {
            cifry[sloupec] = 0;
        }
        /* projdu vsechny polozky v radku a zjistim cisla */
        for (sloupec=0; sloupec<9; sloupec++) {
            /* cifra je uz obsazena? */
            if (cifry[wrk[radek][sloupec]-1] != 0)
                return 0;
            else  /* obsad cifru */
                cifry[wrk[radek][sloupec]-1]=1;
        }
    }
    return 1;
}
 
 
 
/* ------------------------------------------------------------------ */
/* Test, zda je ve vsech blocich 3x3 devet ruznych hodnot             */
/* ------------------------------------------------------------------ */
int testBloky(void)
{
    int blokx, bloky;
    int cifry[9], cifra;
    int i, j;
    for (bloky=0; bloky<9; bloky+=3) {
        for (blokx=0; blokx<9; blokx+=3) {
            memset(cifry, 0, 9 * sizeof(int));
            for (j=0; j<3; j++) {
                for (i=0; i<3; i++) {
                    cifra=wrk[j+bloky][i+blokx];
                    if (cifry[cifra-1])
                        return 0;
                    else  /* obsad cifru */
                        cifry[cifra-1]=1;
                }
            }
        }
    }
    return 1;
}
 
 
 
/* ------------------------------------------------------------------ */
/* Tisk nalezeneho reseni                                             */
/* ------------------------------------------------------------------ */
void tiskVysledku(void)
{
    int i, j;
    putchar('\n');
    ODDELOVAC
    for (j=0; j<9; j++) {
        printf("| ");
        for (i=0; i<9; i++) {
            printf("%d ", wrk[j][i]);
            if (i % 3 == 2) printf(" | ");
        }
        putchar('\n');
        if (j % 3 == 2)
            ODDELOVAC
    }
}
 
 
 
/* ------------------------------------------------------------------ */
/* Reseni Sudoku                                                      */
/* ------------------------------------------------------------------ */
void solve(void)
{
    int i, j, k;
    int done=0;
    double counter=0;
    double max=1;
    int cnt=0;
 
    /* zjisteni poctu kombinaci pro vypis */
    for (j=0; j<9; j++) {
        for (i=0; i<9; i++) {
            int cnt=0;
            for (k=0; k<9; k++) {
                if (povol[j][i][k])
                    cnt++;
            }
            max=max*cnt;
        }
    }
 
    while (!done) {
        /* test, zda je Sudoku vyreseno */
        if (testRadky() && testSloupce() && testBloky()) {
            tiskVysledku();
            return;
        }
 
        /* vypis prubeznych vysledku */
        if (cnt==5e6) {
            printf("Kombinaci: %.0f z %.f  zbyva %.f = %7.3f%%\n", counter, max, max-counter, 100.0 - 100.0*counter/max);
            cnt=0;
        }
        for (j=0; j<9; j++) {
            for (i=0; i<9; i++) {
                if (!src[j][i]) { /* jde o nevyplnenou polozku -> zvysime jeji hodnotu */
                    if (wrk[j][i]<9) {
                        do { /* prejit na dalsi povolenou cifru */
                            wrk[j][i]++;
                            k=wrk[j][i];
                        } while (povol[j][i][k-1]==0 && k<=9);
                        if (k>9 || povol[j][i][k-1]==0) { /* uz jsme na konci rady povolenych cisel? */
                            wrk[j][i]=minim[j][i];        /* prejdeme na dalsi policko */
                        }
                        else {
                            goto KONEC;
                        }
                    }
                    else { /* jsme na konci -> nastavime prvni povolenou cifru */
                        wrk[j][i]=minim[j][i];
                        if (j==8 && i==8) done=1;
                    }
                }
            }
        }
KONEC:
        counter++;
        cnt++;
    }
}
 
 
 
/* ------------------------------------------------------------------ */
/* Vypis vsech kombinaci cisel pro kazde policko                      */
/* ------------------------------------------------------------------ */
void vypisPovol()
{
    double counter=1;
    int i, j, k;
    /* vypsani povol matice */
    for (j=0; j<9; j++) {
        for (i=0; i<9; i++) {
            int cnt=0;
            int prvniCislo=1;
            putchar('(');
            for (k=0; k<9; k++) {
                if (povol[j][i][k]) {
                    if (prvniCislo)
                        prvniCislo = 0;
                    else
                        putchar(' ');
                    putchar('1'+k);
                    cnt++;
                }
            }
            putchar(')');
            putchar(' ');
            putchar(' ');
            counter=counter*cnt;
        }
        putchar('\n');
    }
    printf("Kombinaci celkem: %g\n\n", counter);
}
 
 
 
/* ------------------------------------------------------------------ */
/* Funkce zavolana po startu konzolove aplikace                       */
/* ------------------------------------------------------------------ */
int main(void)
{
    int i, j, k;
 
    /* inicializace pracovni matice */
    for (j=0; j<9; j++)
        for (i=0; i<9; i++) {
            wrk[j][i]=src[j][i];
            if (wrk[j][i]==0) wrk[j][i]=1;
        }
 
    /* inicializace povol matice */
    for (j=0; j<9; j++) {
        for (i=0; i<9; i++) {
            for (k=0; k<9; k++) {
                povol[j][i][k]=1;
            }
        }
    }
    puts("Pocet kombinaci v nevyplnenych polickach:");
    vypisPovol();
 
    /* zruseni polozek v matici SRC */
    for (j=0; j<9; j++) {
        for (i=0; i<9; i++) {
            if (src[j][i]>0)
                for (k=0; k<9; k++) {
                    if (k+1!=src[j][i])
                        povol[j][i][k]=0;
                }
        }
    }
 
    testSloupce0();
    puts("Odstraneni jiz vyplnenych cislic ve sloupcich:");
    vypisPovol();
 
    testRadku0();
    puts("Odstraneni jiz vyplnenych cislic v radcich:");
    vypisPovol();
 
    testBloky0();
    puts("Odstraneni jiz vyplnenych cislic v blocich 3x3:");
    vypisPovol();
 
    /* inicializace minimalnich polozek */
    for (j=0; j<9; j++) {
        for (i=0; i<9; i++) {
            if (src[j][i]==0) { /* volna polozka */
                for (k=0; k<9; k++) {
                    if (povol[j][i][k]) {
                        minim[j][i]=k+1;
                        break;
                    }
                }
            }
        }
    }
 
    /* nastaveni prvnich volnych polozek */
    for (j=0; j<9; j++) {
        for (i=0; i<9; i++) {
            if (src[j][i]==0) /* volna polozka */
                wrk[j][i]=minim[j][i];
        }
    }
 
    puts("Mozne kombinace cisel:");
    vypisPovol();
 
    puts("Jdeme na to ... trpelivost ...");
    solve();
 
    return 0;
}
 
 
 
/* ------------------------------------------------------------------ */
/* finito                                                             */
/* ------------------------------------------------------------------ */

9. Testujeme rychlost překladu

Překladač Tiny C Compiler je slovy svých tvůrců přibližně devětkrát rychlejší než překladač GCC, a to dokonce za předpokladu, že GCC nebude provádět žádné optimalizace (výchozím stavem je tedy přepínač -O0). O tom, do jaké míry je či není toto tvrzení pravdivé, se ale raději přesvědčíme sami. V následujících třech tabulkách jsou zobrazeny časy překladu pro všechny tři výše zmíněné demonstrační příklady. Nejprve byl překlad proveden s využitím Tiny C Compileru a Chibiccu, posléze byl použit GCC bez dalších voleb a poslední dva překlady proběhly taktéž s využitím překladače GCC, ovšem v tomto případě byly použity volby -O3 (optimalizace na rychlost výsledného kódu) a -Os (optimalizace na velikost výsledného kódu). V posledních sloupcích je ukázáno, kolikrát rychlejší je překlad pomocí tcc či Chibiccu:

Překlad aplikace „tth“:

Překlad Čas překladu GCC/tcc GCC/Chibbicc
gcc 1,193s 11× 3,6×
gcc -Os 35,052s 340× 105×
gcc -O3 47,507s 461× 142×
tcc 0,103s    
chibicc 0,333s    

Obrázek 1: Rychlost překladu „tth.c“ (menší sloupec = rychlejší překlad).

Poznámka: férové je pochopitelně porovnání s GCC bez použití optimalizací.

Překlad aplikace „mandelbrot.c“:

Překlad Čas překladu GCC/tcc GCC/Chibbicc
gcc 0,050s 6,25× 1,11×
gcc -Os 0,049s 6,12× 1,08×
gcc -O3 0,079s 9,87× 1,75×
tcc 0,008s    
chibicc 0,045s    

Obrázek 2: Rychlost překladu „mandelbrot.c“.

Překlad aplikace „sudoku.c“:

Překlad Čas překladu GCC/tcc GCC/Chibbicc
gcc 0,095s 6,33× 1,6×
gcc -Os 0,113s 7,53× 1,9×
gcc -O3 0,721s 48× 12,2×
tcc 0,015s    
chibicc 0,059s    

Obrázek 3: Rychlost překladu „sudoku.c“.

10. Zhodnocení rychlosti překladu

Z výše uvedených tabulek je patrné, že tvůrci Tiny C Compileru jsou ve svých tvrzeních o rychlosti svého překladače ještě zbytečně skromní, protože tcc dokázal přeložit aplikaci „tth“ zhruba jedenáctkrát rychleji než GCC, nemluvě o 461× rychlejším překladu v případě, pokud GCC tutéž aplikaci překládal s optimalizacemi (ovšem to skutečně není férové srovnání, jen ukazuje, jak jsou optimalizace časově náročné). Samozřejmě se jedná o dosti mezní případ, který však názorně ukazuje některé přednosti jednoprůchodového překladu (zdá se, že v tomto případě doba překladu roste pouze lineárně s počtem překládaných řádků).

U dalších dvou demonstračních příkladů nejsou rozdíly tak patrné (zrychlení je „pouze“ zhruba šestinásobné), a to zejména proto, že se jedná o relativně malé projekty, u nichž se ještě naplno neprojeví rychlost interní překladové smyčky tcc, protože je nutné provést závěrečné slinkování, které je zhruba stejně rychlé (zpracovávají se tytéž sdílené i statické knihovny atd.).

Naproti tomu Chibicc sice taktéž ve všech případech přeložil program rychleji než GCC (3,6× u prvního příkladu), ovšem rozdíly byly v dalších dvou příkladech pouze nepatrné. V tomto ohledu tedy tento překladač nijak nezazářil, ovšem nutno poznamenat, že se jeho tvůrci nesnažili o dosažení velké rychlosti překladu, na rozdíl od tcc.

11. Vliv (absence) optimalizací na rychlost výsledných programů

Další věc, která nás při porovnávání různých překladačů céčka zajímá, je pochopitelně kvalita výsledného spustitelného binárního kódu (ostatně pokud by nám nezáleželo na rychlosti výsledného programu, nemá asi smysl vůbec zvolit si jazyk C). Zde je zřejmé, že Tiny C Compiler ani Chibicc pravděpodobně nebudou dosahovat kvalit GCC nebo Clangu, zejména při povolení všech optimalizací, které GCC podporuje (v případě Clangu je to ještě markantnější). Ovšem zajímavé bude zjistit a vzájemně porovnat kód vygenerovaný Tiny C Compilerem a Chibiccem s kódem, který byl vytvořen překladačem GCC, ovšem bez zapnutí optimalizací. V ideálním případě by kódy mohly být srovnatelně rychlé, ale opět se o tom raději přesvědčíme změřením doby běhu dvou demonstračních příkladů – výpočet a vykreslení fraktálu a vyřešen Sudoku.

Poznámka: časy byly změřeny na počítači s mikroprocesorem Intel® Core™ i7–8665U CPU @ 1.90GHz.

12. Rychlost vykreslení fraktálu

Po překladu všech variant programu mandelbrot.c ho budeme spouštět následujícím způsobem pro zjištění úrovně optimalizace zdrojových kódů:

$ time ./a.out 4096 4096 256 > /dev/null

Takto vypadají naměřené výsledky:

Spuštění Čas běhu
gcc 6,569s
gcc -Os 4,598s
gcc -O3 4,506s
tcc 6,696s
chibicc 22,780s

Obrázek 4: Rychlost doby běhu výpočtu Mandelbrotovy množiny (kratší sloupec = rychlejší výpočet).

Z předchozí tabulky je možné vyčíst, že strojový kód generovaný překladačem Tiny C Compiler je sice na platformě x86–86 (kterou jsme pro benchmarky použili) pomalejší než GCC, ovšem rozdíl oproti neoptimalizovanému kódu generovanému s využitím GCC není nijak závratně velký (časy jsou prakticky shodné). Když navíc přihlédneme k dobám kompilace, kde tcc jasně vyhrává, je patrné, že Tiny C Compiler není jen další projekt na hraní, ale reálně a kvalitně naprogramovaný nástroj. Horší je tomu v případě Chibiccu, který generuje mnohem pomalejší kód, který je navíc větší v porovnání s ostatními dvěma konkurenty: 20kB oproti cca 16kB v případě GCC a pouhými pěti kilobajty v případě tcc. Chibicc je interně velmi jednoduchý překladač a zde je to jasně patrné.

13. Rychlost vyřešení Sudoku

Naprosto stejným způsobem můžeme změřit dobu běhu prográmku na luštění Sudoku:

Spuštění Čas běhu
gcc 4,860s
gcc -Os 1,335s
gcc -O3 0,902s
tcc 5,431s
chibicc 13,908s

Obrázek 5: Rychlost doby vyřešení Sudoku (kratší sloupec = rychlejší výpočet).

Zde je již tcc znatelněji horší, než výsledek překladače GCC bez zapnutí optimalizací (rozdíl přibližně 12%). A nejpomalejší je, již pravděpodobně podle očekávání, výsledek překladu překladače Chibicc, který se opět nijak zvlášť nevyznamenal.

14. Tiny C Compiler ve funkci „interpretru“

Cílem Tiny C Compileru zajisté není konkurovat ve všech oblastech překladači céčka, který je ústřední součástí nástrojů GCC či Clangu, ovšem v některých případech může být jeho použití výhodné. TCC je možné použít především při vývoji, protože rychlost překladu je tak velká, že u menších aplikací (řádově stovky, tisíce až deseti tisíce řádků) vlastně ani není nutné program dělit do více modulů a pro překlad používat soubory Makefile (toto rozdělení je samozřejmě užitečné, ovšem mnohdy se již při vývoji provede takový návrh modulů, který se posléze ukáže být nepříliš vhodný). Tiny C Compiler také může mít své opodstatnění při tvorbě „přímo spustitelných C programů“, protože i bez provádění optimalizací je výsledkem práce TCC vždy nativně spustitelný kód, který je proveden mnohem rychleji než různé verze bajtkódu (Python, částečně i Java) či přímo interpretovaného kódu (BASH).

Pro tento účel lze použít přepínač -run, takže není nutné striktně oddělovat fázi překladu s fází běhu.

15. Hlášení o chybách a varování

Mohlo by se zdát, že i když Tiny C Compiler negeneruje optimalizovaný kód, mohl by být užitečný pro detekci chyb a potenciálních chyb, které překladače většinou vypisují formou varování (warnings). Tiny C Compiler je totiž obvykle tak rychlý, že dokáže překládat zdrojové kódy prakticky „on-the-fly“ v průběhu jejich editace (a zcela jistě při ukládání). Takže by bylo snadné integrovat tcc do programátorských textových editorů a integrovaných vývojových rozhraní (ať již přímo či přes technologii LSP, s níž jsme se již na stránkách Roota několikrát setkali). Bude tedy zajímavé zjistit, do jaké míry Tiny C Compiler dokáže najít potenciálně problematické části kódu. Jeho možnosti si porovnáme jak s Chibiccem, tak i s klasickým GCC a taktéž s Clangem.

16. Zdrojový kód s množstvím potenciálních problémů

Pro test oznamování chyb a varování použijeme následující zdrojový kód naprogramovaný v jazyku C, který jsem získal z článku Warnings Are Your Friend – A Code Quality Primer dostupného na adrese https://hackaday.com/2018/11/06/war­nings-are-your-friend-a-code-quality-primer/:

/* Author: https://hackaday.com/2018/11/06/warnings-are-your-friend-a-code-quality-primer/ */
 
#include <stdio.h>
 
enum count {NONE, ONE, OTHER};
 
int main(int argc, char **argv)
{
    int ret;
    enum count count = argc - 1;    // [1] signedness silently changed*
 
    switch (count) {
        case NONE:
            printf("%s\n", argc);   // [2] integer printed as string
            ret = 1;
                                    // [3] missing break
        case ONE:
            printf(argv[1]);        // [4] format string is not a string literal
            ret = 2;
            break;
    }                               // [5] enum value OTHER not considered
                                    // [6] no default case
 
    return ret;                     // [7] possibly uninitialized value
}

Tento zdrojový kód je sice napsán v C a plně odpovídá syntaxi tohoto programovacího jazyka, na druhou stranu ovšem obsahuje poměrně velké množství sémantických chyb, které by sice neměly céčkovému překladači zabránit v překladu, ovšem bylo by vhodné, aby na ně překladač vývojáře alespoň upozornil. Tyto potenciální chyby jsou popsány v komentářích přímo ve zdrojovém kódu.

Jak se s tímto problematickým zdrojovým kódem vyrovnají všechny popisované překladače jazyka C. Podívejme se nyní na jejich výpisy chyb a varování v průběhu překladu.

17. Chybová hlášení a varování vypisovaná překladači céčka: GCC a Clang

Nejprve se podívejme na výsledky vypsané překladačem GCC. V první variantě se použil pouze příkaz gcc bez dalších přepínačů:

warnings.c: In function ‘main’:
warnings.c:14:22: warning: format ‘%s’ expects argument of type ‘char *’, but argument 2 has type ‘int’ [-Wformat=]
   14 |             printf("%s\n", argc);   // [2] integer printed as string
      |                     ~^     ~~~~
      |                      |     |
      |                      |     int
      |                      char *
      |                     %d
warnings.c:18:13: warning: format not a string literal and no format arguments [-Wformat-security]
   18 |             printf(argv[1]);        // [4] format string is not a string literal
      |             ^~~~~~

V další variantě již při volání GCC použijeme přepínač -Wall, který povolí detekci a výpis většího množství varování:

warnings.c: In function ‘main’:
warnings.c:14:22: warning: format ‘%s’ expects argument of type ‘char *’, but argument 2 has type ‘int’ [-Wformat=]
   14 |             printf("%s\n", argc);   // [2] integer printed as string
      |                     ~^     ~~~~
      |                      |     |
      |                      |     int
      |                      char *
      |                     %d
warnings.c:18:13: warning: format not a string literal and no format arguments [-Wformat-security]
   18 |             printf(argv[1]);        // [4] format string is not a string literal
      |             ^~~~~~
warnings.c:12:5: warning: enumeration value ‘OTHER’ not handled in switch [-Wswitch]
   12 |     switch (count) {
      |     ^~~~~~
Poznámka: zde se detekuje i problematická část rozeskoku switch.

Pro úplnost přidejme ještě přepínač -Wextra:

warnings.c: In function ‘main’:
warnings.c:14:22: warning: format ‘%s’ expects argument of type ‘char *’, but argument 2 has type ‘int’ [-Wformat=]
   14 |             printf("%s\n", argc);   // [2] integer printed as string
      |                     ~^     ~~~~
      |                      |     |
      |                      |     int
      |                      char *
      |                     %d
warnings.c:18:13: warning: format not a string literal and no format arguments [-Wformat-security]
   18 |             printf(argv[1]);        // [4] format string is not a string literal
      |             ^~~~~~
warnings.c:12:5: warning: enumeration value ‘OTHER’ not handled in switch [-Wswitch]
   12 |     switch (count) {
      |     ^~~~~~
warnings.c:15:17: warning: this statement may fall through [-Wimplicit-fallthrough=]
   15 |             ret = 1;
      |             ~~~~^~~
warnings.c:17:9: note: here
   17 |         case ONE:
      |         ^~~~

Další překladač programovacího jazyka C, který dokáže dobře detekovat potenciální chyby, je překladač Clang. Opět si nejprve ukažme, jaké chyby vypíše tento překladač v případě, že nepoužijeme žádné další přepínače:

warnings.c:8:16: error: cannot initialize a variable of type 'enum count' with an rvalue of type 'int'
    8 |     enum count count = argc - 1;    // [1] signedness silently changed*
      |                ^       ~~~~~~~~
warnings.c:12:28: warning: format specifies type 'char *' but the argument has type 'int' [-Wformat]
   12 |             printf("%s\n", argc);   // [2] integer printed as string
      |                     ~~     ^~~~
      |                     %d
warnings.c:16:20: warning: format string is not a string literal (potentially insecure) [-Wformat-security]
   16 |             printf(argv[1]);        // [4] format string is not a string literal
      |                    ^~~~~~~
warnings.c:16:20: note: treat the string as an argument to avoid this
   16 |             printf(argv[1]);        // [4] format string is not a string literal
      |                    ^
      |                    "%s",
warnings.c:10:13: warning: enumeration value 'OTHER' not handled in switch [-Wswitch]
   10 |     switch (count) {
      |             ^~~~~
3 warnings and 1 error generated.
Compiler returned: 1

Přepínače -Wall zkombinované, popř. s přepínačem -Wextra poskytnou stejný výsledek:

warnings.c:8:16: error: cannot initialize a variable of type 'enum count' with an rvalue of type 'int'
    8 |     enum count count = argc - 1;    // [1] signedness silently changed*
      |                ^       ~~~~~~~~
warnings.c:12:28: warning: format specifies type 'char *' but the argument has type 'int' [-Wformat]
   12 |             printf("%s\n", argc);   // [2] integer printed as string
      |                     ~~     ^~~~
      |                     %d
warnings.c:16:20: warning: format string is not a string literal (potentially insecure) [-Wformat-security]
   16 |             printf(argv[1]);        // [4] format string is not a string literal
      |                    ^~~~~~~
warnings.c:16:20: note: treat the string as an argument to avoid this
   16 |             printf(argv[1]);        // [4] format string is not a string literal
      |                    ^
      |                    "%s",
warnings.c:10:13: warning: enumeration value 'OTHER' not handled in switch [-Wswitch]
   10 |     switch (count) {
      |             ^~~~~
3 warnings and 1 error generated.
Compiler returned: 1

Nejvíce problémů (5 ze sedmi) nalezneme při použití přepínače -Weverything:

warnings.c:8:16: error: cannot initialize a variable of type 'enum count' with an rvalue of type 'int'
    8 |     enum count count = argc - 1;    // [1] signedness silently changed*
      |                ^       ~~~~~~~~
warnings.c:12:28: warning: format specifies type 'char *' but the argument has type 'int' [-Wformat]
   12 |             printf("%s\n", argc);   // [2] integer printed as string
      |                     ~~     ^~~~
      |                     %d
warnings.c:16:20: warning: format string is not a string literal (potentially insecure) [-Wformat-security]
   16 |             printf(argv[1]);        // [4] format string is not a string literal
      |                    ^~~~~~~
warnings.c:16:20: note: treat the string as an argument to avoid this
   16 |             printf(argv[1]);        // [4] format string is not a string literal
      |                    ^
      |                    "%s",
warnings.c:10:5: warning: 'switch' missing 'default' label [-Wswitch-default]
   10 |     switch (count) {
      |     ^
warnings.c:10:13: warning: enumeration value 'OTHER' not handled in switch [-Wswitch]
   10 |     switch (count) {
      |             ^~~~~
4 warnings and 1 error generated.
Compiler returned: 1

18. Chybová hlášení a varování vypisovaná tcc a Chibiccem

Nyní již následují výstupy obou „malých“ překladačů céčka, tedy Tiny C Compileru a Chibicu. Začněme prvním zmíněným překladačem, tedy tcc zavolaným pouze s předáním zdrojového kódu pro překlad (bez dalších přepínačů):

$ tcc warnings.c

V tomto případě jsme nedostali žádné informace o varování. Zkusme tedy přepínač -Wall:

$ tcc -Wall warnings.c

Opět se nevypsalo žádné varování, takže v tomto ohledu Tiny C Compiler dosti zklamal.

A nakonec jsme si nechali „malý“ překladač céčka Chibicc. Ten žádné další přepínače nenabízí, takže je zde naše práce snadná:

bitcoin_skoleni

$ chibicc warnings.c

Opět se nevypsalo absolutně žádné varování ani chybové hlášení.

Poznámka: pro kontrolu kvality či bechybnosti zdrojových kódů tedy tyto dva „malé překladače“ nejsou v žádném případě vhodné.

19. Kde tedy „malé“ překladače použít?

Již jsme si řekli, že Tiny C Compiler lze využít i jako „interpret“ céčka (což je ovšem jen zdání, protože se stále používá fáze překladu). Zajímavé je využití Tiny C Compileru či Chibiccu (či obou!) ve schématu „důvěřivé důvěry“, což je velmi zajímavé téma, kterému jsme se věnovali v tomto článku.

Poznámka: právě kvůli schématu „důvěřivé důvěry“ je tak důležité, aby překladače pro všechny jazyky, které překládají samy sebe (Go, Rust atd.), měly i alternativní překladače.

20. Odkazy na Internetu

  1. Chibicc na GitHubu
    https://github.com/rui314/chibicc
  2. Původní domovská stránka Tiny C Compileru
    https://bellard.org/tcc/
  3. Tiny C Compiler na Wikipedii
    https://en.wikipedia.org/wi­ki/Tiny_C_Compiler
  4. Repositář Tiny C Compileru
    https://repo.or.cz/w/tinycc.git
  5. Warnings Are Your Friend – A Code Quality Primer
    https://hackaday.com/2018/11/06/war­nings-are-your-friend-a-code-quality-primer/
  6. Defending Against Compiler-Based Backdoors
    https://blog.regehr.org/archives/1241
  7. Reflections on Trusting Trust
    https://www.win.tue.nl/~a­eb/linux/hh/thompson/trus­t.html
  8. Coding Machines (povídka)
    https://www.teamten.com/law­rence/writings/coding-machines/
  9. Stage0
    https://bootstrapping.mira­heze.org/wiki/Stage0
  10. Projekt stage0 na GitHubu
    https://github.com/oriansj/stage0
  11. Bootstraping wiki
    https://bootstrapping.mira­heze.org/wiki/Main_Page
  12. Bootstrapped 6502 Assembler
    https://github.com/robinluc­key/bootstrap-6502
  13. IBM Basic assembly language and successors (Wikipedia)
    https://en.wikipedia.org/wi­ki/IBM_Basic_assembly_lan­guage_and_successors
  14. X86 Assembly/Bootloaders
    https://en.wikibooks.org/wi­ki/X86_Assembly/Bootloaders
  15. run6502, lib6502 — 6502 microprocessor emulator
    http://piumarta.com/software/lib6502/
  16. Simple Computer Simulator Instruction-Set
    http://www.science.smith.e­du/dftwiki/index.php/Simple_Com­puter_Simulator_Instructi­on-Set
  17. Bootstrapping#Computing (Wikipedia)
    https://en.wikipedia.org/wi­ki/Bootstrapping#Computing
  18. Bootstrapping (compilers)
    https://en.wikipedia.org/wi­ki/Bootstrapping_%28compi­lers%29
  19. Bootstrapable Builds
    http://bootstrappable.org/
  20. What is a coder's worst nightmare?
    https://www.quora.com/What-is-a-coders-worst-nightmare/answer/Mick-Stute
  21. Linux Assembly
    http://asm.sourceforge.net/
  22. Tombstone diagram (Wikipedia)
    https://en.wikipedia.org/wi­ki/Tombstone_diagram
  23. History of compiler construction (Wikipedia)
    https://en.wikipedia.org/wi­ki/History_of_compiler_con­struction
  24. Self-hosting (Wikipedia)
    https://en.wikipedia.org/wiki/Self-hosting
  25. GNU Mes: Maxwell Equations of Software
    https://gitlab.com/janneke/mes
  26. Tiny C Compiler
    https://bellard.org/tcc/
  27. Welcome to C–
    https://www.cs.tufts.edu/~nr/c--/index.html
  28. c4 – C in four functions
    https://github.com/rswier/c4
  29. Tiny BASIC (Wikipedia)
    https://en.wikipedia.org/wi­ki/Tiny_BASIC
  30. David A. Wheeler’s Page on Fully Countering Trusting Trust through Diverse Double-Compiling (DDC) – Countering Trojan Horse attacks on Compilers
    https://www.dwheeler.com/trusting-trust/
  31. Reviewing Microsoft's Automatic Insertion of Telemetry into C++ Binaries
    https://www.infoq.com/new­s/2016/06/visual-cpp-telemetry
  32. Visual Studio adding telemetry function calls to binary?
    https://www.reddit.com/r/cpp/com­ments/4ibauu/visual_studi­o_adding_telemetry_functi­on_calls_to/d30dmvu/
  33. LWN – The Trojan Horse
    https://www.dwheeler.com/trusting-trust/spencer-19981123.txt
  34. reproducible-builds.org
    https://reproducible-builds.org/
  35. Other Assemblers
    http://asm.sourceforge.net/how­to/other.html
  36. Projekt bootstrap
    https://github.com/ras52/bootstrap
  37. Projekt bcompiler
    https://github.com/certik/bcompiler
  38. Zadní vrátka
    https://cs.wikipedia.org/wi­ki/Zadn%C3%AD_vr%C3%A1tka#Re­flections_on_Trusting_Trust
  39. David A. Wheeler’s Personal Home Page
    https://www.dwheeler.com/
  40. David A. Wheeler
    https://en.wikipedia.org/wi­ki/David_A._Wheeler
  41. Multics Security Evaluation: Vulnerability Analysis
    http://csrc.nist.gov/publi­cations/history/karg74.pdf
  42. Reflections on Rusting Trust
    https://manishearth.github­.io/blog/2016/12/02/reflec­tions-on-rusting-trust/
  43. Reflections on Trusting Trust for Go (slajdy)
    https://www.slideshare.net/y­eokm1/reflections-on-trusting-trust-for-go
  44. Reflections on Trusting Trust for Go (zdrojové materiály)
    https://github.com/yeokm1/reflections-on-trusting-trust-go
  45. Reflections on Trusting Trust for Go – GopherConSG 2018
    https://www.youtube.com/wat­ch?v=T82JttlJf60
  46. Reproducing Go binaries byte-by-byte
    https://blog.filippo.io/reproducing-go-binaries-byte-by-byte/
  47. Trojský kůň
    https://cs.wikipedia.org/wi­ki/Trojsk%C3%BD_k%C5%AF%C5%88_%­28program%29

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.