TT Lab
Get started
Learn Learning paths Courses

Compilers — Build a Small Language from Start to Finish

Count and Read the Output of gcc and clang

Continue in TT Lab

Goal

You build readasm.py, a tool that cuts gcc's -S assembly and clang's LLVM IR by function and counts them, and use it to confirm in numbers the differences between -O0 and -O2 (-O1) — inlining, constant folding, turning division into multiplication, turning tail recursion into a loop, and mem2reg and φ.

Why it matters

If you do not know what optimization did, you only guess when facing a performance problem, and you end up suspecting the compiler when facing a bug that appears only at -O2. If you read the output directly, you can check both. The grader compiles anew at grading time variant C files with the constants and divisors changed, so your tool must read according to the rules to pass.

The rules

어셈블리(gcc -S)   맨 앞이 글자·_·. 인 'name:' 줄이 이름표. '.' 으로 시작하면 안쪽 이름표(.L3 등),
                   아니면 함수의 시작. 들여 쓴 줄 가운데 '.' 으로 시작하지 않는 것이 명령. # 뒤는 주석.
                   명령은 앞뒤 공백을 걷고 명령어와 피연산자 사이 공백을 하나로('movl\t$1, %eax' → 'movl $1, %eax')
functions(asm)     {함수: [명령…]} (이름표·지시어 제외) · count(asm, f) 는 그 수(endbr64 포함)
calls(asm, f)      call·callq 의 대상(나온 순서), '@PLT' 는 뗀다
returns_constant   endbr64 를 뺀 몸체가 [mov(l|q) $값, %eax|%rax ; ret] 이면 그 값, [xor %eax, %eax ; ret] 이면 0, 아니면 None
div_magic(asm, f)  f 안 첫 movabs(q) 의 즉시값을 부호 없는 64비트로 본 '0x' + 16자리 소문자 16진수, 없으면 None
back_edges(asm, f) f 안에서 앞서 나온 안쪽 이름표로 가는 j* 명령의 수
IR(clang -emit-llvm)  'define … @이름(' 부터 맨 앞의 '}' 까지. 이름표 줄('3:')·빈 줄 제외, ';' 뒤는 주석
opcode(명령)       '%5 = add nsw i64 …' → add · 'store …' → store · tail/musttail/notail 은 건너뛴다
opcode_counts      {opcode: 수} · ssa_summary(O0 IR, O1 IR, f) → {"O0": {alloca, load, store, phi}, "O1": {…}}

Steps

  1. Fill in asm_lines(asm) and functions(asm) in /root/mini/readasm.py. Make the material first: gcc -O0 -S -fno-asynchronous-unwind-tables /opt/fixtures/mini/real/opt.c -o opt-O0.s (and likewise for -O2).
  2. Fill in mnemonic, count, and calls — you can see whether the square call disappears at -O2.
  3. Fill in returns_constant — what sum_to, always, and never fold into at -O2.
  4. Fill in div_magic — the grader also looks at variants with the divisor changed.
  5. Fill in back_edges — fact is a call at -O0 and a loop at -O2.
  6. Fill in ir_functions, opcode, and opcode_counts (clang -O0 -S -emit-llvm …, and -O1 too).
  7. Fill in ssa_summary.
  8. Write the values read with your readasm into /root/mini/real_report.json: fib_O0 and fib_O2 (the instruction counts of fib in opt.c), fib_mini (the instruction count of mini_f_fib in the assembly of /opt/fixtures/mini/programs/fib.mini built with the module 8 code generator — via python3 mini.py asm), sum_to_O2, div10_magic, fact_O2_calls (the number of calls), fact_O2_back_edges, and fib_O1_phi (the number of phi in fib in the -O1 IR).

Notes

Cut the assembly into functions

On each line, strip what follows #, and treat a 'name:' that starts at the very beginning as a label. A label starting with '.' is an inner label of the current function, and otherwise it is the start of a new function. Among indented lines, only those that do not start with '.' are instructions, and reduce whitespace to one with split(None, 1).

Instruction count and calls

count is the length of the functions result (endbr64 is also an instruction). calls collects the targets of instructions whose first word is call or callq, stripping what follows '@'. Compare the call list of sum_squares at -O0 and -O2.

A function folded to a single constant

Look only when the body without endbr64 is exactly two instructions and the second is ret. If the first instruction is mov(l|q) $value, %eax (or %rax), it is that value, and if it clears %eax against itself with xor, it is 0.

Multiplication instead of division — the magic number

Look through the function's instructions from the front for movabs (or movabsq) $number, %register. gcc writes it as a signed decimal, so take the remainder mod 2 ** 64 to make it unsigned and write it as '0x%016x'.

Did the recursion become a loop

Looking at the lines of one function in order with asm_lines, collect the inner labels that have appeared so far. If the target of an instruction starting with j is an already seen label, it is a backward jump. In the -O2 fact, the call disappears and this count appears.

Cut LLVM IR into functions

Open a function at a line 'define … @name(' and close it at a line that begins with '}'. For the lines in between, strip what follows ';', and drop those that are empty or are a label like '3:'. For opcode, if there is an '=', the first word after it; skip tail, musttail, and notail.

Count stack slots turning into φ

Get opcode_counts from each of the two IRs and pull out only four — alloca, load, store, and phi (0 if absent). The alloca at -O0 becoming 0 at -O1 and phi appearing is the real thing behind the φ positions you computed in module 7.

gcc and your compiler side by side

Make the material again (gcc -O0/-O2 -S, clang -O1 -emit-llvm, mini.py asm), compute the eight values with your readasm functions, and write them. Do not copy the numbers by hand — the grader rereads with the same compiler and compares.