16/01/2015

Kodować jak w NASA

Home

Kolega podesłał mi link do ciekawego artykułu na temat 10 zasad stosowanych w NASA, aby pisać naprawdę bezpieczny, czytelny i dobry kod. Zasady te w oryginale dotyczą języka C i zacząłem się zastanawiać czy da się je zastosować do .NET'a. Na zielono zaznaczyłem te zasady, które moim zdaniem da się użyć w .NET'cie, na pomarańczowo te dyskusyjne, a na czerwono takie, których się nie da.

Stosuj tylko bardzo proste konstrukcje sterujące przepływem sterowania w programie. Nie używaj goto i rekursji.

Mi ta zasada przypomina inną Keep it simple stupid, czyli nie starajmy się na siłę pokazać jakimi super programistami jesteśmy i piszmy możliwie prosto. To, że bez goto można się obejść to oczywiste. Bardzo dyskusyjny jest natomiast zakaz użycia rekursji. Autor zasady argumentuje to tym, że brak rekursji ułatwia pracę analizatorom kodu źródłowego, a także dowodzenie poprawności kodu. Ciężko mi z tym dyskutować, bo nie wiem jakiego analizatora używa NASA i nie spotkałem się też z dowodzeniem poprawności kodu w .NET. Osobiście uważam, że rekursja jest przydatna i w wielu przypadkach algorytmy zapisane rekurencyjnie są po prostu łatwiejszych do zrozumienia. Trzeba jednak uważać o czym już pisałem w tym albo w tym artykule.

Wszystkie pętle powinny mieć sztywno określoną górną granicę liczby iteracji.

Znowu dyskusyjna sprawa i znowu ma to służyć temu, aby można było udowodnić, że pętla się kiedyś zakończy. Dać się pewnie da tak pisać, ale wygodne to to pewnie nie jest. Ponieważ w .NET'ie nie piszemy oprogramowania dla statków kosmicznych tą zasadę bym pominął.

Nie stosuj dynamicznej alokacji pamięci (w szczególności garbage collector'a) już po zainicjowaniu aplikacji.

No cóż w .NET bez garbage collector'a się nie da. Można próbować minimalizować tworzenie nowych obiektów w czasie działania aplikacji, ale tak na co dzień to właściwie po co? To, co powinno się rozważyć, to załadowanie do pamięci danych niezmiennych (referencyjnych, słownikowych czy jak to zwał) i korzystanie z nich przez cały czas życia aplikacji.

Metody powinny być możliwie krótkie tj. nie więcej niż 60 linii kodu.

Czy 60 to dobre ograniczenie? Można dyskutować, ale z pewnością metody powinny być możliwie krótkie bo to podnosi ich czytelność. Po drugie jeśli metoda jest krótka to znaczy, że robi jedną konkretną rzecz, a nie kilka lub kilkanaście.

Średnia liczba asercji na metodę powinna wynosić minimum 2. Asercje powinny zabezpieczać przez sytuacjami, które w ogóle nie powinny wystąpić i nie mieć efektów ubocznych.

Co do tego jak pisać asercje to się zgadzam, ciężko mi natomiast powiedzieć, czy 2 to dobra średnia asercji na metodę. W komentarzu do tej zasady autor pisze, że przeciętnie testy jednostkowe wykrywają 1 błąd na 10-100 linii kodu, a asercje jeszcze zwiększają szansę na wykrycie błędów. Ja to rozumiem tak, że sugeruje się używanie tego i tego. Ok, ale ja bym jednak explicite wspomniał potrzebę testów jednostkowych czy ogólnie testów automatycznych w tej zasadzie.

Zmienne powinny mieć możliwe mały zasięg.

Czyli nie stosujemy globalnego stanu, nie re-używamy tej samej zmiennej do różnych celów itd.

Wyniki zwracane przez metody powinny być zawsze sprawdzane przez metodę wołającą. Każda wołana metoda powinna zawsze sprawdzać parametry wejściowe.

Innymi słowy nie ufamy nikomu, nie stosujemy konwencji (na przykład takiej, że jeśli metoda zwraca kolekcję to nigdy nie zwróci null'a, ale pustą kolekcję) itp. Ja jednak lubię konwencję, a parametry wejściowe staram się weryfikować głównie dla metod publicznych i chronionych. Wyniki zwracane przez metody weryfikuję natomiast przede wszystkim przy użyciu zewnętrznych bibliotek.

Należy ograniczyć użycie pre-procesora, makr i kompilacji warunkowej.

Preprocesora i makr nie mamy w .NET, ale możliwość kompilacji warunkowej już tak. Czemu ich nie używać? Ponieważ utrudniają przewidzenie wyniku kompilacji. Autor zasady podaje taki przykład. Załóżmy, że w programie mamy 10 dyrektyw kompilacji warunkowej. Załóżmy, że używają innych warunków. Daje 2^10 = 1024 różnych możliwych wyników kompilacji tego samego kodu, które mogą działać inaczej!

Należy ograniczyć użycie wskaźników. W szczególności stosujemy co najwyżej jeden poziom dereferencji. Wskaźniki na funkcje nie są dozwolone.

Autor ponownie argumentuje, że brak wskaźników ułatwia weryfikację kodu. Tą zasadę ciężko jednak przełożyć na .NET. Niby z typowych wskaźników też można korzystać, ale nie jest to często używane. Jeśli natomiast pod słowo ''wskaźnik'' w tej regule podstawimy ''referencja'', a pod ''wskaźnik na funkcję'' terminy ''delegat'' lub ''wyrażenia lambda'' to okaże się, że w .NET nie możemy zrobić właściwie nic. Podsumowując ta zasada nie ma zastosowania do .NET'a.

Kod musi się kompilować i to bez ostrzeżeń. Przynajmniej raz dziennie kod źródłowy musi przejść weryfikację przy pomocy narzędzi do statycznej analizy kodu z zerową liczbą ostrzeżeń.

To, że kod musi się kompilować to oczywiste. Jeśli chodzi o ostrzeżenia to moim zdaniem w przypadku projektów prowadzonych od zera liczba ostrzeżeń rzeczywiście powinna być równa 0. Jeśli uważamy, że jakieś ostrzeżenia są "głupie" to trzeba odpowiedzieć sobie na pytanie czy używamy dobrych narzędzi? W przypadku tzw. kodu zastanego sprawa jest trudniejsza, ale te 10 zasad służy m.in. właśnie temu, aby nie tworzyć takiego kodu.

Jak widać nie wszystkie z tych zasad da się zastosować w .NET czy przy tworzeniu aplikacji typowo biznesowych. Z drugiej strony są firmy, które w .NET piszą oprogramowanie medyczne. Niektóre z tych zasad wydają się bardzo drakońskie, ale jak już pisałem NASA w swoich projektach osiąga wynik 0 błędów na tysiąc linii produkcyjnego kodu.

Polecam też zapoznanie się z oryginalnym dokumentem NASA’s 10 rules for developing safety-critical code.

08/01/2015

Czy sposób pisania pętli for ma wpływ na wydajność?

Home

Przy okazji codziennej prasówki natknąłem się na ten artykuł na temat wydajności pętli for w JavaScript'cie dla różnych przeglądarek. W skrócie chodzi o to czy powinniśmy pisać pętlą for tak:
for (int i = 0; i < array.Length; i++)
   ...
A może raczej tak, czyli zapamiętać długości tablicy w zmiennej pomocniczej i nie odczytywać jej przy każdym przebiegu pętli:
for (int i = 0, len = array.Length; i < len; i++)
   ...
Z ciekawości sprawdziłem czy taka mikro optymalizacja ma jakiekolwiek znaczenie w przypadku programowania na platformę .NET. Do zmierzenia czasu użyłem takiej metody pomocniczej:
public static void MeasureIt<T>(Action<T> action, T arg, int noOfIterations)
{
   var sw = new Stopwatch();
   sw.Start();

   for (var i = 0; i < noOfIterations; ++i)
      action(arg);

   sw.Stop();
   Console.WriteLine("Total time: " + sw.ElapsedMilliseconds);
}
Następnie wykonałem następujący test:
var noOfIterations = 100000;
var noOfElements = 10000;
var array = Enumerable.Repeat(1, noOfElements).ToArray();
//Przypadek 1
MeasureIt(arg =>
{
   var total = 0;
   for (var i = 0; i < arg.Length; i++)
      total += a[i];  
}, array, noOfIterations);
//Przypadek 2
MeasureIt(arg =>
{
   var total = 0;
   for (int i = 0, len = arg.Length; i < len; i++)
      total += a[i];
}, array, noOfIterations);
Dla rzetelności testy uruchomiłem wielokrotnie. Czas wykonywania obliczeń w obu przypadkach wynosił około 320ms z dokładnością do kilku milisekund. Czasami pierwsze podejście było szybsze, a czasami drugie. Z jednej strony czegoś takiego się spodziewałem. Z drugiej strony sądziłem, że jednak zaobserwuję pewien zysk w drugim przypadku. Dlaczego?

Otóż jeśli spojrzymy na kod pośredni wygenerowany przez kompilator to te dwie implementacje bynajmniej nie są identyczne. W pierwszym przypadku długość tablicy odczytywana jest wielokrotnie na koniec pętli, a w drugim tylko raz przed właściwą pętlą. Najwidoczniej jest to jednak tak szybka operacja, że się nie liczy (w IL do odczytania długości jednowymiarowej tablicy istnieje dedykowana instrukcja ldlen).

Dodatkowo przeprowadziłem podobny test dla użycia listy generycznej List<T>. Tym razem różnice były już zauważalne i wynosiły około 27% na rzecz zapamiętania liczby elementów listy w dodatkowej zmiennej. Średnio pierwsza wersja wykonywała się 957ms, a druga 752ms. Wynika to z tego, że aby odczytać liczbę elementów w liście należy odczytać właściwość Count czyli metodę get_Count. Na poziomie IL jest to robione przy pomocy instrukcji callvirt (w telegraficznym skrócie służącej do wołania metod na rzecz obiektów), a nie dedykowanej (i pewnie zoptymalizowanej) instrukcji ldlen jak ma to miejsce w przypadku tablic. Pomimo tych różnic uważam jednak, że w codziennej praktyce programistycznej nie należy się tym przejmować gdyż różnice w czasach obliczeń, w porównaniu do dużej liczby iteracji (100000), są zbyt małe.

24/12/2014

Życzenia świąteczne

Home

Kolejne Święta Bożego Narodzenia już tuż tuż. W tym roku czekam na nie jeszcze bardziej niż zwykle, bo to pierwsze wydarzenie tego rodzaju dla naszej córeczki. Wszystkim czytającym mojego bloga i nie tylko życzę dużo radości w gronie bliskich Im osób, a w Nowym Roku ciekawych wyzwań zawodowych oraz zapału do nauki i doskonalenia się.

Serdecznie Pozdrawiam,
Michał Komorowski

21/12/2014

Czego prawdopodobnie nie wiedzieliście o Excel'u

Home

Sądzę, że wielu z Was otarło się na studiach o programowanie liniowe oraz algorytm sympleks. Ja uczyłem się o tym na przedmiocie zwanym w skrócie POBO, co rozwija się dumnie brzmiące Podstawy badań operacyjnych. Od czasów studiów nie zajmowałem się tym zagadnieniem, aż do dzisiaj. Pomagając siostrze w rozwiązywaniu zadań na studia dowiedziałem się o możliwościach Excel'a, których w ogóle nie byłem świadomy, a są naprawdę super i każdy ma do nich dostęp. Mam tutaj na myśli dodatek Solver, który, między innymi, implementuje algorytm sympleks w bardzo przystępnej formie. Tyle tytułem wstępu. Spójrzmy na prosty przykład.

Zaczynamy od uruchomienia Excel'a. Następnie klikamy tą fikuśną okrągłą ikonę w lewym górnym roku okna i wybieramy Opcje programu Excel. Dalej przechodzimy do zakładki Dodatki i klikamy przycisk Przejdź.



W oknie, jakie się pojawi, wybieramy Dodatek Solver i zatwierdzamy.



Po zatwierdzeniu w zakładce Dane na wstążce pojawi się nowa opcja.



Teraz spróbujmy rozwiązać przykładowe proste zadanie. Załóżmy, że mamy 5 fabryk i chcemy znaleźć lokalizację centrum dystrybucyjnego tak aby suma odległości od wszystkich fabryk była minimalna. Dodatkowe ograniczenie jest takie, że odległość od każdej z fabryk nie może być większa niż 60. Położenia fabryk podane są we współrzędnych kartezjańskich. Odległość pomiędzy fabrykami, a centrum obliczamy przy pomocy standardowego wzoru. Sytuacja początkowa wygląda tak. Dla ułatwienia naniosłem położenia fabryk i początkowe położenie centrum na wykres.



Teraz uruchamiamy Solver. Jako komórkę celu wybieram pole z sumą odległości i zaznaczam, że tą wartość chcę minimalizować. Jako komórki zmieniane wybieram współrzędne centrum. Dodajemy też ograniczenie na odległość każdej z fabryk od centrum. Na koniec uruchamiam obliczenia i klikam Rozwiąż.



Wynik końcowy wygląda w następujący sposób:



To tylko wierzchołek góry lodowej. Dodatek Solver ma dużo większe możliwość i wiele opcji. Można go wykorzystać do harmonogramowania, zdefiniować wiele ograniczeń, ustalić maksymalny czas obliczeń, dokładność uzyskanego wyniku i wiele więcej. Sądzę, że warto sobie zapamiętać, że Excel ma takie możliwości i w razie potrzeby doczytać i douczyć się jak z tego korzystać.

08/12/2014

Wykrywanie nieużywanych elementów bibliografii

Home

Moim zdaniem LaTeX ma wspaniałą obsługę cytowań i bibliografii. Jest to jeden z powodów, dla którego tak lubię go używać np.: bardzo łatwo znaleźć cytowania w formacie rozumianym przez LaTeX'a, nie musimy martwić się o formatowanie, odpowiednie posortowanie czy numerowanie.

Domyślnie jest dodamy jakąś pozycję do bazy danych odnośników literaturowych, a jej nie zacytujemy to w finalnym dokumencie wygenerowanym przez LaTeX'a (np.: PDF) zostanie ona pominięta. Może to być zachowanie pożądane lub nie. Jeśli nie jest z pomocą przychodzi komenda \nocite, w szczególności jej forma \nocite{*}, która powoduje, że cokolwiek dodamy do spisu odnośników to znajdzie się to w finalnym dokumencie w sekcji z bibliografią. W pewnym momencie możemy jednak chcieć uporządkować bazę odnośników i sprawdzić co używamy, a czego nie. Przy dużym dokumencie, z dziesiątkami lub setkami cytowań nie jest to sprawa oczywista.

W takiej sytuacji z pomocą przychodzi, odkryty przeze mnie ostatnio, pakiet refcheck. Po jego włączeniu dla każdego nieużywanego elementu bibliografii, ale także dla każdej nieużywanej etykiety zostanie wygenerowane ostrzeżenie. Dodatkowo nieużywane elementy zostaną oznaczone w wygenerowanym dokumencie przy pomocy etykiet na marginesie dokumentu.

Kolejny raz okazuje się, że czegokolwiek bym sobie nie zamarzył to istnieje już pakiet, który to zapewnia :)