Zbyt wiele w „projektowaniu" Darktable zaczynało się od „fajnie by było, gdybyśmy mogli…". Powiem Ci, co jest fajne: jak najszybsze powieszenie Twoich dobrych zdjęć na ścianie. Sztuki wizualne nie są sztuką wykonawczą (jak muzyka czy teatr), więc liczy się tylko rezultat. Wszystko, co następuje wcześniej, to narzut, który zwykle chcesz utrzymać na minimalnym poziomie. Nie znaczy to, że proces sam w sobie nie może sprawiać przyjemności. Aby jednak czerpać przyjemność z procesu, musisz opanować swoje narzędzia i nagiąć je do swojej woli, w przeciwnym razie tylko z nimi walczysz, a cały proces sprowadza się do frustracji. Problem w tym, że „projektowanie" Darktable wkłada zbyt wiele wysiłku w bycie innym dla samej odmienności.
W tym procesie dodawania „fajnych nowości" Darktable popsuł skróty klawiszowe i wiele podstawowych zachowań interfejsu, zastępując czysty kod spaghetti i dodając więcej bałaganu w GUI, nigdy niczego nie usuwając.
Ansel ma jawny proces projektowania, który obowiązkowo zaczyna się od zdefiniowanych problemów napotkanych przez zdefiniowanych użytkowników. Okazuje się, że ilość kodu do napisania jest odwrotnie proporcjonalna do ilości przemyśleń poświęconych rozwiązaniu, zwykle po to, by wychwycić źródłowy problem z tego, co mówią użytkownicy, i znaleźć najprostszą drogę do rozwiązania (którym często nie jest nawet rozwiązanie programowe…).
Ale błędy nie czekają na Ciebie w myśleniu, czekają tylko w kodzie, który napisałeś. Zatem im więcej myślisz, tym mniej kodujesz i tym mniejszy ciężar utrzymania tworzysz sobie na przyszłość. Ale oczywiście… musisz mieć wystarczająco dużo czasu, by wszystko przemyśleć. Zasadniczo oznacza to pożegnanie z amatorskim dłubaniem w sobotnie popołudnia!
Nie rozbudowuj tego, jeśli najpierw nie potrafisz tego uprościć
Wiele dłubania w Darktable odbywało się przez kopiowanie i wklejanie kodu z innych części oprogramowania, a nawet z innych projektów, głównie dlatego, że współtwórcy nie mają czasu ani umiejętności, by podjąć się dużych przepisań. Powoduje to duplikację kodu i zwiększa długość funkcji, dodając wewnętrzne rozgałęzienia i wprowadzając if oraz switch case zagnieżdżone czasem na ponad 4 poziomach, przez co strukturę i logikę trudniej pojąć, a błędy trudniej (i frustrująco) wytropić, przy jednoczesnym zwiększeniu prawdopodobieństwa ich wystąpienia.
Tak czy inaczej, gdy kod odpowiedzialny za istniejące funkcje tylko rośnie (czasem dziesięciokrotnie w ciągu 4 lat), rodzi to poważne pytania o przyszłą utrzymywalność, w kontekście, w którym współtwórcy zostają nie dłużej niż kilka lat, a deweloperzy mają ograniczony czas do zainwestowania. To po prostu nieodpowiedzialne, bo poświęca długoterminową utrzymywalność na rzecz błyszczących nowinek.
Upraszczanie i uogólnianie kodu za pomocą czystych API przed dodaniem nowych funkcji jest koniecznością, a Ansel przyjmuje tylko kod, który osobiście rozumiem i mam umiejętności, by go utrzymać. KISS.
Podstawowa logika kodowania
Pull requesty, które nie spełniają minimalnych wymagań jakości kodu, nie będą przyjmowane. Wymagania te mają na celu zapewnienie długoterminowej utrzymywalności i stabilności poprzez wymuszenie przejrzystego, czytelnego kodu o prostej strukturze logicznej.
Procedury należy w miarę możliwości dzielić na jednostkowe, wielokrotnego użytku funkcje. Wyjątkiem są wyspecjalizowane procedury liniowe (bez rozgałęzień) wykonujące zadania zbyt specyficzne, by je gdziekolwiek ponownie wykorzystać, ale w takim przypadku użyj komentarzy, by rozbić procedury na „rozdziały" lub kroki, które łatwo dostrzec i zrozumieć.
Funkcje powinny realizować tylko jedno zadanie naraz. Na przykład kod GUI nie powinien być mieszany z kodem SQL ani z kodem przetwarzającym piksele. Gettery i settery powinny być osobnymi funkcjami.
Funkcje powinny mieć tylko jeden punkt wejścia i jeden punkt wyjścia (return). Jedynymi akceptowanymi wyjątkami są wczesny powrót, jeśli bufor pamięci, na którym funkcja ma operować, nie jest zainicjalizowany, lub jeśli blokada muteksu wątku jest już przechwycona.
Funkcje powinny mieć czytelne, jednoznaczne nazwy oraz nazwy argumentów, które zdradzają ich przeznaczenie. Programy są przeznaczone do czytania przez ludzi; jeśli kodujesz dla maszyny, rób to w kodzie binarnym.
Funkcje mogą zagnieżdżać maksymalnie 2 struktury warunkowe if. Jeśli potrzeba więcej niż 2 zagnieżdżonych if, strukturę kodu należy przemyśleć na nowo i prawdopodobnie rozbić na bardziej szczegółowe funkcje.
if powinno testować wyłącznie jednorodne przypadki, takie jak stan lub wartość najlepiej jednej (ale może i większej liczby) zmiennej(-ych) tego samego typu. Jeśli trzeba przetestować przypadki niejednorodne (jak IF user param IS value AND picture buffer IS initialized AND picture IS raw AND picture HAS embedded color profile AND color profile coeff[0] IS NOT NaN), należy je przenieść do funkcji sprawdzającej zwracającej gbooleanTRUE lub FALSE i odpowiednio nazwanej, aby inni deweloperzy rozumieli cel sprawdzenia bez dwuznaczności przy pobieżnym czytaniu kodu, jak color_matrix_should_apply(). Kod rozgałęzienia będzie wtedy wyglądał tak: if(color_matrix_should_apply()) pix_out = dot_product(pix_in, matrix);
Komentarze powinny wyjaśniać, dlaczego zrobiłeś to, co zrobiłeś, np. Twoje wyjściowe założenia, Twoje powody oraz wszelkie odniesienia naukowe lub dokumentacyjne, na których się oparłeś (powinny się tam znaleźć numery DOI i adresy URL). Twój kod powinien wprost mówić, co zrobiłeś. Jeśli musisz w komentarzach wyjaśniać, co robi Twój kod, zwykle jest to znak, że kod jest źle ustrukturyzowany, zmienne i funkcje są źle nazwane itd.
Szybkie obejścia, które ukrywają problemy zamiast rozwiązywać je u źródła, nie będą przyjmowane. Jeśli Cię takie interesują, rozważ raczej wkład w bazowy projekt darktable. Jedynymi wyjątkami będą sytuacje, gdy problemy są blokujące (powodują awarię programu) i nie znaleziono lepszego rozwiązania po przyzwoitym czasie poświęconym na badania.
Zawsze pamiętaj, że najlepszy kod to najprostszy kod. KISS. Aby osiągnąć ten cel, zwykle lepiej napisać kod od zera, niż próbować zszywać kawałki istniejącego kodu przez intensywne kopiowanie i wklejanie.
Każdy wie, że debugowanie jest dwa razy trudniejsze niż samo napisanie programu. Więc jeśli piszesz go tak sprytnie, jak tylko potrafisz, to jak kiedykolwiek go zdebugujesz?— Brian W. Kernighan
Każdy głupiec potrafi napisać kod, który zrozumie komputer. Dobrzy programiści piszą kod, który rozumieją ludzie.— Martin Fowler, Refactoring: Improving the Design of Existing Code
Zawsze koduj tak, jakby osobą, która ostatecznie będzie utrzymywać Twój kod, był agresywny psychopata, który wie, gdzie mieszkasz.— John Woods
Za każdym razem, gdy muszę się zastanowić, by zrozumieć, co robi kod, pytam sam siebie, czy mogę go zrefaktoryzować, aby to zrozumienie stało się od razu widoczne.— Martin Fowler, Refactoring: Improving the Design of Existing Code
Kod jest zły. Gnije. Wymaga okresowego utrzymania. Ma błędy, które trzeba znaleźć. Nowe funkcje oznaczają, że stary kod trzeba dostosować. Im więcej masz kodu, tym więcej miejsc, w których błędy mogą się ukryć. Tym dłużej trwają pobrania (checkout) lub kompilacje. Tym więcej czasu zajmuje nowemu pracownikowi zrozumienie Twojego systemu. Jeśli musisz refaktoryzować, jest więcej rzeczy do przeniesienia.
Kod jest wytwarzany przez inżynierów. Aby powstało więcej kodu, potrzeba więcej inżynierów. Inżynierowie mają koszty komunikacji rzędu n^2, a cały ten kod, który dodają do systemu, choć rozszerza jego możliwości, zwiększa też cały koszyk kosztów. Powinieneś zrobić wszystko, co możliwe, by zwiększyć produktywność poszczególnych programistów pod względem siły wyrazu kodu, który piszą. Mniej kodu do zrobienia tego samego (i być może lepiej). Mniej programistów do zatrudnienia. Mniejsze organizacyjne koszty komunikacji.
Dobrzy programiści piszą dobry kod. Wielcy programiści nie piszą kodu. Programiści zen usuwają kod.— John Byrd
Specyficzna logika kodowania w C
Ansel, podobnie jak darktable, jest napisany w C. Ten język jest przeznaczony dla zaawansowanych programistów do pisania szybkich błędów w systemach operacyjnych i aplikacjach systemowych. Daje zbyt dużo swobody, by robić szkodliwe rzeczy, i nie da się go debugować przed uruchomieniem programu ani bez napisania własnych testów (które same mogą być błędne albo mogą faworyzować pewien rodzaj błędów, które przepuszczają, a zresztą nikt nie pisze testów). Tymczasem większość współtwórców nie jest wyszkolona w C, wielu z nich nie jest nawet zawodowymi programistami (choć zawodowych programistów C prawdopodobnie nie powinno się dopuszczać w pobliże aplikacji dla użytkownika końcowego), więc C jest niebezpiecznym językiem dla każdej aplikacji open source.
C pozwoli Ci pisać do buforów, które nie zostały zaalokowane (co skutkuje błędem segfault) i pozwoli Ci zwolnić je więcej niż raz, ale nie zwolni buforów, gdy nie są już potrzebne (co skutkuje wyciekami pamięci, jeśli zapomniałeś zrobić to ręcznie). Problem w tym, że skoro alokacja/zwolnienie bufora może być odległe (zarówno w czasie życia programu, jak i w kodzie źródłowym) od miejsca, w którym go używasz, łatwo to zepsuć. C pozwoli Ci też rzutować dowolny wskaźnik na dowolny typ danych, co umożliwia wiele błędów programisty i uszkodzenie danych. Natywne metody obsługi łańcuchów znaków nie są bezpieczne (z powodów, których nigdy nie chciało mi się zrozumieć), więc musimy używać tych z GLib, aby zapobiec lukom bezpieczeństwa.
Zasadniczo C czyni Cię swoim własnym i najgorszym wrogiem, i to na Tobie spoczywa przestrzeganie zasad bezpieczeństwa, których sens stanie się jasny dopiero, gdy je złamiesz. Zupełnie jak błędy w programie w C. Przyjmij, że piszesz swój kod tak, by czytali go laicy, którzy nigdy wcześniej nie programowali w C.
Musisz też pamiętać, że kompilator wykona większość optymalizacji za Ciebie, ale będzie wobec nich bardzo zachowawczy. Zasada jest taka, że jeśli Twój kod jest łatwo zrozumiały dla człowieka (prosta logika), zostanie właściwie zrozumiany przez kompilator, który podejmie odpowiednie środki optymalizacyjne. Odwrotnie, ręczne optymalizacje w kodzie, dające zagadkowy kod uznawany za szybszy na systemach jednowątkowych, zwykle obracają się przeciwko Tobie i po kompilacji dają wolniejsze programy.
Wzorce i struktury
Pętle for są zarezerwowane do iterowania po tablicach o znanej z góry wielkości, tak by liczba kroków pętli była znana. Rozciągając tę logikę, mogą być też używane do iterowania po elementach GList * (które nie mają właściwości rozmiaru, ponieważ są alokowane dynamicznie), choć sprawdza się wtedy, czy każdy element (GList *)->next nie jest NULL. Pętle for na ogół nie powinny używać instrukcji break ani return w swoim przepływie sterowania, chyba że pętla szuka konkretnego elementu w tablicy i zwraca go, gdy tylko zostanie znaleziony. Jeśli Twoja pętla ma warunek zatrzymania, użyj while.
C nie jest językiem obiektowym, ale możesz i powinieneś stosować logikę obiektową tam, gdzie to zasadne, używając struktur do przechowywania danych oraz wskaźników do metod, a następnie jednolitych getterów i setterów do definiowania i uzyskiwania dostępu do danych.
struktury takie jak while, for, if czy switch nie powinny być zagnieżdżone na więcej niż 3 (a najlepiej 2) poziomach. Jeśli tak się dzieje, użyj funkcji:
Zawsze uzyskuj dostęp do danych z buforów za pomocą składni tablicowej, od ich wskaźnika bazowego, zamiast używać niestałych wskaźników, na których wykonujesz arytmetykę. Na przykład zrób tak:
1float*const buffer =malloc(64*sizeof(float));
2for(int i =0; i <64; i++)
3{
4 buffer[i] = ...
5}
Nie rób tak:
1float*buffer =malloc(64*sizeof(float));
2for(int i =0; i <64; i++)
3{
4*buffer++= ...
5}
Ta druga wersja jest nie tylko mniej czytelna, ale też uniemożliwi zrównoleglenie i optymalizacje kompilatora, ponieważ wartość wskaźnika zależy od iteracji pętli i musiałaby być współdzielona między wątkami, jeśli takie istnieją. Pierwsza wersja prowadzi do logiki dostępu do pamięci niezależnej od iteracji pętli i może być bezpiecznie zrównoleglona.
Używanie wbudowanych inkrementacji zmiennych (zobacz koszmarny przykład tutaj ) jest surowo zabronione, chyba że jest to jedyna operacja w wierszu. Są one bałaganem prowadzącym do wielu błędów programistycznych. To jest dozwolone:
1uint32_t counter;
2for(int i =0; i <64; i++)
3{
4if(array[i] > threshold)
5 counter++;
6}
Instrukcje case w strukturze switch nie powinny być kumulatywne. Nie rób tak:
Przy pobieżnym czytaniu nie będzie od razu jasne, że przypadek VALUE3 dziedziczy klauzule zdefiniowane przez poprzednie przypadki, zwłaszcza w sytuacjach, gdy przypadków jest więcej. Zrób tak:
Każdy przypadek jest samodzielny, a wynik nie zależy od kolejności deklaracji przypadków.
Sortuj i przechowuj swoje zmienne w strukturach, które przekazujesz jako argumenty funkcji, zamiast używać funkcji z więcej niż 8 argumentami. Nie rób tak:
Pierwszy przykład pochodzi z darktable . Kopiowanie i wklejanie wywołań funkcji jest zbędne, a mnożenie argumentów pozycyjnych sprawia, że niemożliwe jest zapamiętanie, który jest który. Nie pokazuje też, które argumenty są stałe w różnych gałęziach, co utrudni refaktoryzację. Drugi przykład nie jest bardziej zwięzły, jednak struktura nie tylko ułatwia wywołanie funkcji, ale deklaracja struktury pozwala jawnie ustawić każdy argument, z wbudowanymi sprawdzeniami, jeśli to potrzebne. Zależność argumentów wejściowych od warunków zewnętrznych jest również od razu jasna, a argumenty logiczne są ustawiane bezpośrednio z warunków, co ułatwi rozszerzanie programu w przyszłości i uczyni go mniej podatnym na błędy programistyczne wynikające z nieporozumień co do zależności zmiennych.
Optymalizacje OpenMP
Piksele to w istocie czterowymiarowe wektory RGBA. Od 2004 roku procesory mają specjalne możliwości przetwarzania wektorów i stosowania pojedynczej instrukcji na wielu danych (SIMD). Pozwala to przyspieszyć obliczenia, przetwarzając jednocześnie cały piksel (SSE2) aż do 4 pikseli (AVX-512), oszczędzając wiele cykli CPU.
Nowoczesne kompilatory mają opcje automatycznej wektoryzacji, które potrafią optymalizować czysty kod C, a biblioteka OpenMP pozwala dostarczać wskazówki poprawiające to, pod warunkiem że kod jest napisany w sposób wektoryzowalny i używa pewnych pragm.
unikaj rozgałęzień w pętlach, które zmieniają przepływ sterowania. Używaj wbudowanych instrukcji jak absolute = (x > 0) ? x : -x;, aby mogły być przekształcone w maski bajtów w SIMD,
piksele powinny być adresowane wyłącznie od wskaźnika bazowego ich tablicy oraz indeksów pętli, tak abyś mógł przewidzieć, do jakiego adresu pamięci następuje dostęp, wyłącznie na podstawie indeksu pętli,
unikaj przekazywania argumentów typu struct w funkcjach wywoływanych w pętlach OpenMP i rozpakuj składowe struct przed pętlą. Wektoryzacji nie da się wykonać na strukturach, a jedynie na skalarach i tablicach float oraz int. Na przykład:
1typedef struct iop_data_t
2{
3float[4] pixel DT_ALIGNED_PIXEL; // wyrównanie na adresach 16-bitowych
4float factor;
5} iop_data_t;
6 7#ifdef _OPENMP 8#pragma declare simd 9#endif10/* zadeklaruj funkcję jako wektoryzowalną i uczyń ją inline, aby uniknąć wywołań z wnętrza pętli */11inline float foo(constfloat x, constfloat pixel[4], constfloat factor)
12{
13float sum = x;
1415/* użyj redukcji SIMD, aby zwektoryzować sumę */16#ifdef _OPENMP17#pragma omp simd aligned(pixel:16) reduction(+:sum)18#endif19for(size_t k =0; k <4; ++k)
20 sum += pixel[k];
2122return factor * sum;
23}
2425void loop(constfloat*const restrict in,
26float*const restrict out,
27const size_t width, const size_t height,
28const struct iop_data_t bar)
29{
30/* rozpakuj składowe struktury */31constfloat*const restrict pixel = bar->pixel;
32constfloat factor = bar-> factor;
3334#ifdef _OPENMP35#pragma omp parallel for simd default(none) \36 dt_omp_firstprivate(in, out, pixel, factor, width, height) \
37 schedule(simd:static) aligned(in, out:64)
38#endif39for(size_t k =0; k < height * width; ++k)
40 {
41 out[k] = foo(in[k], pixel, factor);
42 }
43}
jeśli używasz pętli zagnieżdżonych (np. pętla po szerokości i wysokości tablicy), deklaruj wskaźniki pikseli w najbardziej wewnętrznej pętli i użyj collapse(2) w pragmie OpenMP, aby kompilator mógł zoptymalizować użycie pamięci podręcznej/pamięci i równomierniej podzielić pętlę między różne wątki,
w miarę możliwości używaj płaskiego indeksowania tablic (for(size_t k = 0 ; k < ch * width * height ; k += ch)) zamiast zagnieżdżonych pętli szerokość/wysokość/kanały,
używaj słowa kluczowego restrict na wskaźnikach obrazu/pikseli, aby uniknąć aliasingu, i unikaj operacji w miejscu na pikselach (*out musi zawsze różnić się od *in), aby nie wywoływać zależności zmiennych między wątkami
wyrównuj tablice na blokach 64 bajtów, a piksele na blokach 16 bajtów, aby pamięć była ciągła, a CPU mógł ładować pełne linie pamięci podręcznej (i unikać segfaultów),
pisz małe funkcje i optymalizuj lokalnie (jedna pętla/funkcja), używając OpenMP i/lub pragm kompilatora,
utrzymuj swój kod maksymalnie prosty, systematyczny i unikaj przemądrzałej arytmetyki wskaźników, bo doprowadzi to jedynie do wykrycia przez kompilator zależności zmiennych i aliasingu wskaźników tam, gdzie ich nie ma,
unikaj rzutowań typów w pętli,
deklaruj wskaźniki wejściowe/wyjściowe jako *const, a zmienne jako const, aby uniknąć fałszywego współdzielenia (false-sharing) w pętlach równoległych (przy użyciu pragmy OpenMP shared(variable)).
Formatowanie kodu
Używaj spacji zamiast tabulatorów,
Wcięcia to 2 spacje,
Usuwaj końcowe spacje,
{ i } umieszczaj w osobnym wierszu,
Wytyczne
Rób rzeczy, które opanowałeś: tak, miło jest uczyć się nowych rzeczy, ale Ansel nie jest piaskownicą, jest oprogramowaniem produkcyjnym i nie jest właściwym miejscem na naukę.
KISS i bądź leniwy: Ansel nie ma 50 deweloperów na pełny etat, minimalizm zarówno w funkcjach, jak i w objętości kodu jest rozsądny i zdrowy dla obecnego zarządzania, ale też dla przyszłego utrzymania. (KISS: keep it stupid simple).
Rób jak reszta świata: jasne, jeśli wszyscy wyskakują przez okno, masz prawo za nimi nie iść, ale większość problemów dotyczących UI/UX oprogramowania została już gdzieś rozwiązana i w większości przypadków sensownie jest po prostu ponownie użyć tych rozwiązań, bo większość użytkowników już je zna.
Programowanie nie jest celem: programowanie jest środkiem do celu, a celem jest możliwość przetworzenia dużej liczby zdjęć w krótkim czasie przy jednoczesnym uzyskaniu pożądanego wyglądu każdego zdjęcia. Zadania programistyczne należy traktować jako narzut i utrzymywać na minimalnym poziomie, a objętość kodu jest obciążeniem dla każdego projektu.
Translated from English by :
Claude.
In case of conflict, inconsistency or error, the English version shall prevail.