Kompilacja to właściwie nic innego jak tłumaczenie z jednego języka na drugi. W przypadku C# (a dokładniej .net-a) to tłumaczenie odbywa się dwa razy. Raz z języka czytelnego dla kompilatorów aminokwasowych (c#) na MSIL czyli na taki w dużym uproszczeniu zarządzalny asembler (zrozumiały przez rzadko którego białkowca) a drugi raz z owego MSIL do takiego już prawdziwego kodu maszynowego, który jest zrozumiały dla krzemiaków.
Każdy program w C# może mieć praktycznie nieskończenie wiele wersji w MSIL-u (to tak jak z książkami, przekłady różnych autorów rzadko są jednakowe). Jeśli nie wierzysz, to zdekompiluj swój kod i powrzucać w przypadkowe miejsca “nop-y” Wynikowy program będzie działał tak samo mimo, że z punktu widzenia kodu, mamy już inny. W przypadku C# podczas tłumaczenia kompilator musi wymyślać identyfikatory dla różnych dziwnych rzeczy (anonimowe klasy, typy, metody etc). W sytuacji kiedy kompilator musi wymyślać owe identyfikatory, wówczas korzysta z pewnych strategii. Owe strategie zależą już od konkretnej implementacji kompilatora i tak na prawdę jest to detal – aktualne, które są używane zostały opisane tutaj. Kolejne identyfikatory generowane są z kolejnym numerem ( wg. wzorca P<N>C__SI gdzie I jest unikatowym numerem, w szczególności kolejnym – jest to najłatwiejszy i najtańszy sposób na zapewnienie unikalności). Jedna ważna uwaga, identyfikatory muszą być unikatowe w ramach assembly.
Kompilacja
Biorąc co powyższe pod uwagę, to jeśli kompilator otrzyma pliki w różnej kolejności – otrzymamy różną sekwencję identyfikatorów. Kompilator nie sortuje plików jakoś specjalnie. W sytuacji gdy wywołamy csc *.cs zdajemy się na kolejność z jaką pliki zostaną zwrócone przez system – co znowu nie zawsze musi zwrócić tą samą kolejność.
Najlepsze na koniec, unikatowy identyfikator będzie unikatowy jeśli będziemy kompilować w ramach jednego wątku. Co jeśli zaczniemy kompilować wielowątkowo? Budując drzewo zależności możemy spokojnie kompilować assembly, klasy czy nawet metody równolegle na wielu procesorach. Nigdzie w specyfikacji nie jest (na szczęście) zapisane, że kompilacja musi się wykonywać jednowątkowo.
Koniec końców kompilator również za każdą kompilacją wrzuca unikatowy GUID wykorzystywany np. przez pliki pdb. Czyli również i dlatego nie otrzymamy tego samego kodu.
Oczywiście to nie koniec z kompilacją. Przecież podczas uruchamiania aplikacji taki sam proces ma miejsce – do pracy wchodzi jiit, który wykonuje bardzo podobną pracę. Przy okazji może korzystać z informacji na temat środowiska, w którym aplikacja jest uruchamiana dzięki czemu może wykonywać bardzo specyficzne optymalizacje.
A teraz najstraszniejsze, c# jest przepisywany niejako od nowa (*). Przy okazji wychwytywane są wszelkie rozjazdy pomiędzy specyfikacją a implementacją. Więc z dużą dozą prawdopodobieństwa można założyć, że dotychczasowe szczegóły implementacji zostaną zmienione.
(*) dzięki projektowi Roslyn ale o tym (jak zwykle) innym razem.