[Post gościnny by mulander] 12 listopada 2009 roku Gynvael napisał o swoich pierwszych wrażeniach dotyczących języka Go. Po upływie ponad roku napomknąłem autorowi, iż ciekawym tematem byłby analogiczny post na temat języków Digital Mars D oraz Mozilla Rust. Cała ta sytuacja przypomniała mi o pierwotnym poście i nakłoniła do odświeżenia lektury. Natknąwszy się na zdanie "Po za tym szkoda trochę, że język jest jeszcze niedostępny na platformę Windows." stwierdziłem, że warto sprawdzić jak sprawy się obecnie mają.

Parę minut spędzonych na oficjalnej stronie Go pozwala nam odkryć link omawiający port języka na platforme MS Windows.

Z podlinkowanej strony pobieramy zatem instalator gowin32_2011-03-07.1_installer.exe, a następnie instalujemy go w naszym systemie.

Gynvael w ramach rekonesansu języka przeportowal jeden ze swoich raytracerów. Moim zamiarem była kompilacja oraz uruchomienie jego kodu z pozytywnym wynikiem na platformie MS Windows z ewentualnym naniesieniem wszelkich potrzebnych zmian celem uzyskania wyniku zgodnego z pierwowzorem.

Ponieważ t2.go rozpoczyna się serią deklaracji makr preprocessora sprawdziłem, czy Go doczekalo się analogicznej funkcjonalności.
Przegląd listy dyskusyjnej oraz bugtrackera wykazuje, że nadal jest to celowo unikana funkcjonalność.
Fakt ten wymusza na nas przetworzenie pliku według zaleceń autora standardowym preprocesorem cpp oraz usunięciem definicji makr za pomocą grepa (osobna instalacja MinGW).

Wykonujemy zatem w powłoce MinGW:

$ cpp t2.go | grep -v '^#' > t2.out.go
W dalszej częsci tej noty zakładam zamianę nazwy pliku t2.out.go na t2.go.

Następnie ze zwykłej linii poleceń cmd.exe próbujemy wykonać kolejny krok z instrukcji w nagłówku programu:

C:\blog>6g t2.go
Nazwa '6g' nie jest rozpoznawana jako polecenie wewnętrzne lub zewnętrzne,
program wykonywalny lub plik wsadowy.

Nic nadzwyczajengo.
Pobraliśmy 32 bitową wersję Go (zresztą, innej nie widziałem na stronie pobierania).
Twórcy języka przy dobieraniu nazw programów kierowali się schematem nazewnictwa przyjętym z systemu plan9, zatem 6 w 6g odwołuję się do amd64 (x86-64, zaś g wskazuje, że jest to kompilator języka Go.
W tymże wypadku należy skorzystać z kompilatora 8g oraz linkera 8l, (gdzie 8 odpowiada architekturze x86).

Ponawiamy więc próbę kompilacji z użyciem poprawnego programu:

C:\blog>8g t2.go
t2.go:57: syntax error: unexpected semicolon or newline before {
t2.go:60: non-declaration statement outside function body
t2.go:62: non-declaration statement outside function body
t2.go:65: syntax error: unexpected semicolon or newline before {
t2.go:65: non-declaration statement outside function body
t2.go:66: non-declaration statement outside function body
t2.go:67: non-declaration statement outside function body
t2.go:68: non-declaration statement outside function body
t2.go:70: syntax error: unexpected semicolon or newline before {
t2.go:70: too many errors

Powyższe problemy kompilacji wynikają ze zmian, jakie zaszły w języku w ciągu roku jego rozwoju.
Wartym odnotowania jest komunikat t2.go:70: too many errors.
Nie jest to bynajmniej wskazanie, że w linii 70 zamieściliśmy ogromną ilość błedów :) Kompilator informuje nas, iż błędów jest więcej niż wyświetlił, jednakże zaprzestanie informowania o kolejnych dopóki nie poprawimy obecnie wypisanych.
Większość osób parających się dłużej programowaniem z doświadczenia wie, iż poprawa pierwszego raportowanego błędu często eliminuje znaczną ilość kolejnych lub odkrywa ich prawdziwą przyczynę.
Wyplucie wszystkich błędów przez kompilator na ekran jednym rzutem sprawia nierzadko problemy początkującym programsitom próbującym poprawiać błędy od ostatniego widocznego - W wypadku Go domyślne zachowanie kompilatora sprawi, iż mniej osób będzie szukało przysłowiowego wiatru w polu. Zaobserwujmy to na przykładzie pierwszego zestawu błedów:

t2.go:57: syntax error: unexpected semicolon or newline before {
t2.go:60: non-declaration statement outside function body
t2.go:62: non-declaration statement outside function body

Poniżej zawartośc linii 56, 57 oraz 58:

56: func main()
57: {
58:   fmt.Printf("Simple RT by gynvael.coldwind//vx (http://gynvael.coldwind.pl)\n");

Komunikat można przetłumaczyć jako błąd składni: niespodziewany średnik lub znak nowej linii przed {.
Odwołując się do dokumentacji ze strony projektu czytamy:

One caveat. You should never put the opening brace of a control structure (if, for, switch, or select) on the next line. If you do, a semicolon will be inserted before the brace, which could cause unwanted effects. Write them like this:

if i < f() {
   g()
}

not like this

if i < f()  // wrong!
{              // wrong!
   g()
}

Zatem, według autorów - po instrukcjach kontrolnych if, for, switch oraz select kompilator automatycznie dostawia średnik, jeżeli napotka znak nowej linii.
Nie ma tutaj wzmianki o definicji funkcji, zatem sięgamy do udokumentowanych zmian pomiędzy wydaniami języka.

W notatce z dnia 2009-12-22 czytamy:

Since the last release there has been one large syntactic change to
the language, already discussed extensively on this list: semicolons
are now implied between statement-ending tokens and newline characters.


Szeroką dyskusję na temat wprowadzanych zmian w regułach dotyczących stawiania średników można znaleźć na liście dyskusyjnej.

Poniżej ciekawy fragment z dodatkowym uzasadnieniem wproawdzonych zmian:

Believe it or not, this removes rules.  Robert played with
this in the parser and the language spec and was surprised
how much simpler things got.  I am doing the same conversion
in the 6g compiler right now, and the dead code I'm cutting
away is one of the ugliest parts of the compiler.  I'd
forgotten writing it, but boy is it ugly.  And soon it will
be gone. -- Russ Cox


Ciekawym jest brak wzmianki o formatowaniu w przypadku nagłówka funkcji.
Na liście dyskusyjnej, pojawia się wręcz przykład wskazujący, iż powyższa składnia jest poprawna.
       Dla nas natomiast ważnym jest podane przez autora notki polecenie, które pozwala dostosować stary kod do podanych reguł.

gofmt -oldparser -w *.go
Zanim dokonamy próby tranformacji kodu za pomocą gofmt spróbujmy poprawić jedynie linię 57 celem obserwacji zmian w raportowanych błędach:

56: func main() {
57:   fmt.Printf("Simple RT by gynvael.coldwind//vx (http://gynvael.coldwind.pl)\n");

C:\blog>8g t2.go
t2.go:94: syntax error: unexpected semicolon or newline before {
t2.go:97: syntax error: unexpected {
t2.go:114: syntax error: unexpected }
t2.go:116: non-declaration statement outside function body
t2.go:116: empty top-level declaration
t2.go:117: non-declaration statement outside function body
t2.go:117: too many errors

Poprzedni listing prezentował błędy do linii 70, w tym komunikat o takiej samej treści dla linii 65, która jest poprawna.
Obecna lista rozpoczyna się od linii 94, zatem zaoszczędziliśmy sobie konieczności wyszukiwania sporej ilości nieistniejących błędów. Byłaby ona jeszcze większa, gdyby kompilator Go nie przerwał raportowania błędów.

Wycofajmy teraz wprowadzoną przez nas poprawkę i zobaczmy, jak dużo poprawi gofmt:

C:\blog>gofmt -oldparser -w t2.go
flag provided but not defined: -oldparser
usage: gofmt [flags] [path ...]
 -tabwidth=8: tab width
 -trace=false: print parse trace
 -r="": rewrite rule (e.g., '+-[+-:len(+-)] -> +-[+-:]')
 -tabindent=true: indent with tabs independent of -spaces
 -s=false: simplify code
 -l=false: list files whose formatting differs from gofmt's
 -w=false: write result to (source) file instead of stdout
 -ast=false: print AST (before rewrites)
 -spaces=true: align with spaces instead of tabs
 -comments=true: print comments

Powyższy błąd flaga podana lecz nie zdefiniowana: -oldparser. Wskazuje na brak obsługi wymienionej w release notes flagi -oldparser.
Notatka z dnia 2010-01-13 ujawnia usunięcie flagi -oldprinter, lecz nie jest to nasza flaga.
W całym dokumencie występuje tylko jedna instancja tekstu oldparser, zatem usunięcie obsługi flagi mogło zostać nieudokumentowane.
Pozostaje nam wykonać próbę bez jej podawania mając nadzieję, że jest to domyślnie wykonywana operacja:

C:\blog>gofmt -w t2.go
t2.go:57:1: expected declaration, found '{'
..... dalsza lista rozpoznanych problemów

Najwyraźniej mamy pecha.
Narzędzie gofmt domyślnie nie poprawia omawianych przez nas błedów.
Nie zgłaszamy tego autorom, prawdopodobnie odesłano by nas do starszej wersji narzędzia lub zaproponowano modyfikację za pomocą własnego skryptu/ręczną (wniosek na podstawie obsługi zgłoszenia z Grudnia 2009).
Pozostaje więc poprawa klamer otwierających funkcje oraz tych przy pozostałych raportowanych instrukcjach (np. if).
Sposób poprawy pozostawiam wam jako ćwiczenie (dla leniwych link do patcha).

Po poprawie ostatniego błędu związanego z umieszczeniem klamerki kompilator wydrukuje kolejny ciekawy zestaw błędów:

C:\blog>8g t2.go
t2.go:56: ".ggg....." not used
t2.go:56: ".g...rrr." not used
t2.go:56: ".g.g.r.r." not used
t2.go:56: ".ggg.rrr." not used
t2.go:56: "........." not used
t2.go:411: ".ggg....." not used
t2.go:411: ".g...rrr." not used
t2.go:411: ".g.g.r.r." not used
t2.go:411: ".ggg.rrr." not used
t2.go:411: "........." not used
t2.go:56: too many errors

Komunikaty wskazują na to, że ciągi znaków w liniach 56 oraz 411 nie są wykorzystywane.
Wskazane linie prowadzą do początku oraz końca funkcji main, czyli nie pomagają w lokalizacji błędnego kodu.
Na szczęście nie mamy dużo wystąpień wydrukowanego tekstu, możemy poszukać go ręcznie.
Prowadzi nas to do linii 81:

81: SpherePosMap =
82: "........."
83: ".ggg....."
84: ".g...rrr."
85: ".g.g.r.r."
86: ".ggg.rrr."
87: ".........";

W świetle wcześniejszych informacji można wnioskowac, iż problem ponownie wynika z automatycznego dostawiania znaków średnika. Uzasadnienie jest następujące:
Jeżeli w linii 82 kompilator dostawi znak średnika linii, to ciąg znaków zawierający tylko kropki zostanie wpisany do zmiennej SpherePosMap.
Następnie kompilator dostawi średniki do pozostałych linii zawierających definicje kolejnych ciągów znaków, które nie są nigdzie przypisywane, a więc nieużywane.
Kompilator raportuje jako pierwszą błędną linię tę zawierającą ciąg ".ggg.....", czyli pomija całkiem pierwszą przypisywaną według nas do SpherePosMap. Jest to wystarczającym powodem, by wypróbować modyfikacje poprzez dodanie operatora łączenia ciągów znaków (+:

81: SpherePosMap =
82: "........." +
83: ".ggg....." +
84: ".g...rrr." +
85: ".g.g.r.r." +
86: ".ggg.rrr." +
87: ".........";

Po ponownej próbie kompilacji napotykamy następny problem:

C:\blog>8g t2.go
t2.go:348: img.Width undefined (type *image.RGBA has no field or method Width)
t2.go:349: img.Height undefined (type *image.RGBA has no field or method Height)

t2.go:357: img.Height undefined (type *image.RGBA has no field or method Height)

t2.go:360: img.Width undefined (type *image.RGBA has no field or method Width)
t2.go:400: img.Pixel undefined (type *image.RGBA has no field or method Pixel)

Wygląda na to, że zmianie uległ interfejs pakietu image.
Potwierdza to wpis na blogu release notes z dnia 2010-08-11, który opisuje szereg zmian w pakiecie:

An image.Image now has a Bounds rectangle, where previously it ranged
from (0, 0) to (Width, Height). Loops that previously looked like:


for y := 0; y < img.Height(); y++ {
   for x := 0; x < img.Width(); x++ {
       // Do something with img.At(x, y)
   }
}

should instead be:

b := img.Bounds()
for y := b.Min.Y; y < b.Max.Y; y++ {
   for x := b.Min.X; x < b.Max.X; x++ {
       // Do something with img.At(x, y)
   }
}

* image: change image representation from slice-of-slices to linear buffer,
introduce Decode and RegisterFormat,
introduce Transparent and Opaque,
replace Width and Height by Bounds, add the Point and Rect types.


Opisane modyfikacje biblioteki zmuszają do pewnych zmian w funkcji Render we wskazanych przez kompilator liniach, zatem do listy deklarowanych zmiennych dodajemy b w celu uniknięcia wielokrotnego wywoływania img.Bounds.Max/Min.X/Y bezpośrednio w kodzie:

var b = img.Bounds();
Oraz podmieniamy wystąpienia img.Width() oraz img.Height() odpowidenio na:

b.Max.X // odpowiednik img.Width()
b.Max.Y // odpowiednik img.Height()

Dodatkowo, w linii 361 z powodów czysto 'kosmetycznych' zamieniamy:
      x = b.Min.X
na:

b.Min.X
Po wykonaniu powyższych czynności pozostaje tylko jeden problem do rozwiązania:

t2.go:400: img.Pixel undefined (type *image.RGBA has no field or method Pixel)
Wzmianka o zmianach w Pixel pojawia się w postaci pierwszego zdania drugiej częsci opisu zmian pakietu image:

* image: change image representation from slice-of-slices to linear buffer
Niestety, w tym wypadku twórcy nie dostarczyli przykładów modyfikacji jakie należy wykonać na kodzie. Lokalizujemy zatem pakiet image.go instalowany wraz z naszym kompilatorem i sprawdzamy jak wygląda sekcja związana z obsługą struktury image.RGBA.

/* Lokalizacja: C:\Go\src\pkg\image\image.go:28 */
// An RGBA is an in-memory image of RGBAColor values.
type RGBA struct {
   // Pix holds the image's pixels. The pixel at (x, y) is Pix[y*Stride+x].
   Pix    []RGBAColor
   Stride int
   // Rect is the image's bounds.
   Rect Rectangle
}

Przeglądając powyższy fragment kodu możemy wnioskować, iż nazwa pola Pixel uległa zmianie na Pix, natomiast zgodnie z opisem jego format został zamieniony na bufor liniowy zamiast tabeli dwu wymiarowej.
Komentarz zamieszczony w strukturze rónież zawiera przykład wykorzystania nowego formatu:

Pix[y*Stride+x]
Zatem zamieniamy linię 401 naszego programu z:

img.Pixel[y][x] = cl;
na:

img.Pix[y*img.Stride+x] = cl;
I po raz kolejny podejmujemy próbę kompilacji programu:

C:\blog>8g t2.go
Tym razem brak błędów, zatem przechodzimy do linkowania:

C:\blog>8l t2.8
Efektem powyższych poleceń jest plik wykonywalny 8.out.exe, który z radością uruchamiamy :)

C:\blog>8.out.exe
Simple RT by gynvael.coldwind//vx (http://gynvael.coldwind.pl)
Creating scene...
Rendering...
[0] Thread start
[1] Thread start
[2] Thread start
[3] Thread start
[0] Thread finished
[1] Thread finished
[2] Thread finished
[3] Thread finished
Writing test.png image...
Done.

Wynik programu w postaci pliku png możecie obejrzeć pod tym adresem.
W trakcie pracy programu możemy zauważyć, że wykorzystuje on tylko jeden z dwóch (w moim wypadku) dostępnych procesorów.

Kod po dokonaniu wszelkich koniecznych zmian w celu uruchomienia znajduje się tutaj.

Na tym etapie, możemy zastanowić się jak sprawić, by program poprawnie korzystał ze wszystkich dostępnych procesorów.
Z pomocą ponownie przychodzi oficjalna dokumentacja (sekcja Concurrency->Parallelization):

The current implementation of gc (6g, etc.) will not parallelize this code by default.

W wolnym tłumaczeniu:
Obecna implementacja gc (6g, itd.) nie dokonuje domyślnie zrównoleglenia kodu. Na stronie znajdują się przykłady na to, jak takie zrównoleglenie zapewnić ręcznie oraz informacja o tym, iż planowane jest wprowadzenie automatyzacji tej czynności w przyszłości.

Spróbujmy zatem zmodyfikowac kod Gynvaela.
Dodajemy import pakietu runtime na początku pliku oraz definiujemy stałą określającą liczbę procesorów w naszej maszynie:

import (
 "os";
 "fmt";
 "math";
 "image";
 "runtime"; // Dodany import runtime w celu zrównoleglenia renderowania
 "image/png"
)
const NCPU = 2; // Liczba procesorów

Następnie na początku funkcji main() dodajemy wywołanie GOMAXPROCS, przekazując ilość procesorów, jaką dysponujemy:

func main() {
 runtime.GOMAXPROCS(NCPU)
 fmt.Printf("Simple RT by gynvael.coldwind//vx (http://gynvael.coldwind.pl)\n");

Po rekompilacji oraz ponownym zlinkowaniu łatwo można zauważyć poprawne wykorzystywanie obu procesorów przez program.
Zyskujemy około 4 sekund na wykorzystaniu dodatkowego procesora (~20s zamiast ~24s na wyrenderowanie grafiki).
Co ciekawsze, w pierwotnej wersji, według drukowanych przez program traceów, wątki kończyły swoją pracę zawsze w tej samej kolejności - Nowa wersja wykazuje większą różnorodność.
Renderowany plik test.png jest identyczny, (diff nie zwraca żadnych różnic).

Zmodyfikowany kod, znajduje się tutaj.

Warto nadmienić, że ten sam efekt można uzyskać poprzez ustawienie zmiennej środowiskowej GOMAXPROCS, ponieważ jednak nie jest to czynność najwygodniejsza pod platformą MS Windows, ja zdecywowałem się na modyfikację kodu.

Na tym etapie moglibyśmy zakończyć całą zabawę, jednakże dokonamy jeszcze ponownego formatowania kodu za pomocą polecenia gofmt:

C:\blog>gofmt t2.go > t2_p3.go
diff -u wykazuje na zmiany we wcięciach poszczególnych sekcji kodu, usunięcie wszystkich wystąpień średników (które wraz z rozwojem języka stały się opcjonalne) oraz rozmieszczenie klamer bloków lokalnych.
Kod został rozbity do 627 linii z 416, które osiągneliśmy po ostatniej modyfikacji - Według mnie zyskując znacznie na czytelności.

Zabieg ten usprawni zapewne kolejne przeróbki wynikające ze zmian w składni języka zakładając, że będą one wykonywane wystarczająco często (zanim obsługa fazy przejściowej nie zostanie usunięta z narzędzi pomocniczych).

Ostateczna wersja programu po modyfikacjach może być pobrana z tego adresu.

Zainteresowanym podaję ponadto link do skompilowanych na moim systemie (MS Windows Vista 32-bit) plików .exe:
    * t2p1.exe (wersja korzystająca z jednego procesora)
    * t2p2.exe (wersja korzystająca z dwóch procesorów)
    * t2p3.exe (wersja korzystająca z dwóch procesorów po potraktowaniu gofmt)

Pomimo kodu dawno nieodświeżanego pod kątem ciągle rozwijanego języka, ilość i stopień trudności wymaganych zmian w kodzie są zadziwiająco mało problematyczne.
Zadowalająca jest również jakość komunikatów błędów zwracanych przez kompilator - Przeważnie wskazują one dokładne miejsce powstania problemu nie zasypując zbędnymi informacjami.
Także opublikowany ostatnio post na oficjalnym blogu języka informuje o przejściu do prac nad większą stabilizacją języka, zatem oficjalne wydania będą publikowane raz na miesiąc (a nie tak jak dotychczas - raz na tydzień), natomiast dla zainteresowanych najnowszymi zmianami nadal pozostaną dostępne wydania typu weekly ukazujące się raz w tygodniu. Twórcy zapowiadają poza tym wprowadzenie narzędzia gofix, które odpowiedzialne będzie za modyfikacje kodu, więc większość czynności opisanych w tejże notce powinna w przyszłości być zbędna :)

PS.
W trakcie przygotowywania tego postu natknąłem się również na inną implementację kompilatora języka Go pod platformę MS Windows, a mianowicie erGo, który jest o tyle ciekawy, że został zaimplementowany w Go oraz wspiera debugowanie w Visual Studio 2008.
Niestety nie miałem okazji przetestować tej implementacji.

PS2.
Dziękuje neme za przeredagowanie posta :)

by mulander

Comments:

2011-04-09 17:56:50 = piotr
{
go wymiata.
uwielbiam pisać w tym języku.

minimalistyczna składania; żadnego bawienia* się w średniki czy pliki nagłówkowe, do tego go-routines i te ich kanały... a na koniec dostaję jeden .exe, który się uruchamia w mgnieniu oka - jestem w niebie. :)

[*edit by gyn]
}
2011-04-09 20:07:35 = anonim
{
ja tam i tak wole C, co tradycja to tradycja
}
2019-01-08 15:14:36 = l0l3k
{
error 404 przy próbue pobrania t2p2 i innych
}
2019-01-09 20:57:28 = Gynvael Coldwind
{
@l0l3k
Kurcze, nie mam tych plików na dysku. Spytam mulandera.
}
2019-01-12 10:35:31 = Gynvael Coldwind
{
@l0l3k
OK, mulander znalazł pliki na dysku, więc poprawiłem linki :)
}

Add a comment:

Nick:
URL (optional):
Math captcha: 9 ∗ 5 + 10 =