C++Guns – RoboBlog

24.01.2016

Warum (kurze) Arrays in Fortran (und C/C++) nicht immer die richtige Wahl ist

Filed under: Allgemein — Tags: — Thomas @ 22:01

Der Name ist Programm.
Heute mal tex und pdf Version.

wkfanidrwi.pdf

\documentclass[]{article}
\usepackage{listings}
\usepackage[utf8]{inputenc}
\usepackage{ngerman}

%opening
\title{Warum (kurze) Arrays in Fortran (und C/C++) nicht immer die richtige Wahl ist}
\author{Thomas Huxhorn}


\begin{document}

\maketitle

\tableofcontents

\begin{abstract}
In diesem kleinen Artikel lege ich ein paar Gedanken nieder über einen Fehler mit 
(kurzen) Arrays, der systematisch immer wieder Auftritt. Die Wahl der Programmiersprache
ist unabhängig für diese Art von Fehler.
\end{abstract}

\section{Ein Fehler}
Der Fehler selbst lässt sich in einem Satz beschreiben: Einer Funktion wurde nur ein Wert
übergeben, obwohl sie ein Array von zwei Werten erwartet. Das klingt erstmal recht banal, hat es aber in sich.
Hier der entsprechende Beispiel Code in Fortran:

\begin{lstlisting}[frame=single]
subroutine func(data)
  integer, intent(in) :: data(2)
  write(*,*) data(1), data(2)
end subroutine

program test
  integer :: data(2)
  data(1) = 1
  data(2) = 2
  call func(data(1))
end program
\end{lstlisting}

Das Programm compiliert ohne Warnings, ohne Laufzeitfehlermeldungen und keinerlei Überprüfungen die 
der Compiler bereit stellt schlagen Alarm. Sogar die Ausgabe des Programms ist richtig. 
Benutzt wurde der neuste GNU Fortran Compiler in der Version 5.2.1

\begin{lstlisting}[frame=single]
$ gfortran -Wall -Wextra -fcheck=all 
-fsanitize=address,undefined wkfanidrwi1.f95
$ ./a.out 
1           2
\end{lstlisting}

\section{Ein unsichtbarer Fehler}
Auch der memory error detector valgrind findet keinerlei Fehler. Der Grund ist ganz einfach: Aus technischer
Sicht existiert in diesem Programm auch kein Fehler! Das ist auf den ersten Blick etwas verwirrend. 
Der Programmierer wollte, dass die Subroutine func() nur die erste Zahl im Array data verarbeitet. Die Subroutine 
hingegen, erwartet nicht nur zwei Zahlen, sie nimmt sich auch zwei Zahlen! Selbst, wenn man ihr nur eine gibt.
Wir haben hier also gleichzeitig zwei Arten von Fehler. Der Programmierer hat eine Vorstellung, was passieren soll.
Seine Kenntnis über die Subroutine func ist aber falsch, so dass er zu wenig Daten übergibt. Das ist der erste Fehler.
Der zweite Fehler ist, dass der Programmierer die Subroutine so erstellt hat, dass sie immer genau so viele Daten
verarbeitet, wie sie braucht. Egal ob sie auch genügend Daten bekommt. Die beiden Fehler heben sich gegenseitig auf,
und es kommt auch das richtige Ergebnis raus. Kein Programm kann den Fehler finden.

\section{Ein versteckter Fehler}
Heutzutage sind Programme riesig. Sie bestehen aus einer Millionen Zeilen Code, sind über Jahrzehnte gewachsen und kein
Mensch blickt mehr in allen Details durch. Diese Art Fehler zu erkennen ist also menschlich nicht möglich. Ein 
Computer Programm könnte es aber. Der Compiler sieht beim Compilieren das gesamte Programm, den kompletten Quellcode.
Man muss den Compiler aber auch seine Arbeit tun lassen, und nicht selbst die Arraygröße bestimmen.

\section{Lösungversuch}
Die erste Idee ist also, das die Subroutine nicht ein Array fester Größe, sondern eins variabler Größe erwartet. So kann zur
Laufzeit erkannt werden, ob zu viele oder zu wenige Daten übergeben wurden. Hier eine Beispiel Implementierung.

\begin{lstlisting}[frame=single]
module testmodule
contains
subroutine func(data)
integer, intent(in) :: data(:)
write(*,*) data(1), data(2)
end subroutine
end module

program test
use testmodule
integer :: data(2)
data(1) = 1
data(2) = 2
call func(data(1))
! call func(data(1:1))
end program
\end{lstlisting}

Sie Subroutine muss dazu in ein Modul gepackt werden, sonst können keine assumed-shape Arrays benutzt werden.

\begin{lstlisting}[frame=single]
$ gfortran -Wall -Wextra -fcheck=all wkfanidrwi1.f95
wkfanidrwi2.f95:14:12:

call func(data(1))
1
Error: Rank mismatch in argument 'data' at (1) 
(rank-1 and scalar)
\end{lstlisting}

Schon allein das Compilieren führt hier zu einem Fehler. Aber nur, weil ein Array erwartet, aber ein Skalar übergeben wurde. Das kann man ganz leicht aus tricksen, in dem man ein Array der Länge 1 übergibt. Dieser Fall wird dann, wie
erwartet, zur Laufzeit abgefangen.

\begin{lstlisting}[frame=single]
$ ./a.out 
At line 5 of file wkfanidrwi2.f95
Fortran runtime error: Index '2' of dimension 1 of array
'data' above upper bound of 1
\end{lstlisting}

Der Fehler wird jetzt zur Laufzeit erkannt. Das ist schon einmal ein riesiger Fortschritt, als wenn der Fehler nie erkannt wird. Allerdings muss nun bei jedem Array Zugriff die Länge des Arrays überprüft werden. Im Durchschnitt verdoppelt das die Laufzeit des Programms. Und der Fehler wird auch nur gemeldet, wenn entsprechende Checks im
Compiler eingeschaltet wurden. \\
Können wir das nicht besser machen? Immerhin WISSEN wir ja, wie viele Daten die Subroutine brauch. Das muss sich
ausnutzen lassen.

\section{Lösung allgemein}
Die allgemeine Lösung für diese Art von Fehler besteht darin, einfach keine Arrays für so wenige Daten zu nutzen.
Um dennoch nicht jede einzelne Zahl als Parameter zu übergeben, und damit den Code unnötig groß zu machen,
werden die Daten in einem neuen Datentyp gruppiert. \\
Da der Datentyp zwangsläufig einen Namen braucht, können wir den Variablen gleichzeitig eine Bedeutung geben. So ist
ein weiterer Fehler ausgeschlossen, dass man z.B. aus versehen data(1) statt data(2) benutzt.
Hier die Beispiel Implementierung:

\begin{lstlisting}[frame=single]
module testmodule
type Uhrzeit_t
integer :: minute, stunde
end type
contains
subroutine func(uhrzeit)
type(Uhrzeit_t), intent(in) :: uhrzeit
write(*,*) uhrzeit%stunde, uhrzeit%minute
end subroutine
end module

program test
use testmodule
type(Uhrzeit_t) :: uhrzeit
uhrzeit%minute = 1
uhrzeit%stunde = 2
call func(uhrzeit)
end program
\end{lstlisting}

Der ursprüngliche Fehler ist nun nicht mehr möglich. Unnötig Laufzeiteinverschlechterungen sind auch nicht mehr vorhanden. Der Compiler kann durch Typenüberprüfungen zur Compilezeit feststellen, ob die Subroutine immer den
richtigen Datentype erhält. \\

\section{Ausblick}
So weit so gut. Mit der Implementierung dem Uhrzeit Modul haben wir aber viele weitere Fehlerquellen erschaffen.
In der Praxis reicht es nicht einfach nur Stunde und Minute auszugeben. Datum und Uhrzeit müssen verglichen, umgerechnet, verrechnet und erstellt werden. Für all das existieren schon fertige Module, teilweise standardisierte und alle getestet. 


\section{Lösung Qt}

Qt bietet hierfür die Klasse QTime an.
Hier ein Beispiel wie ein Zeitobjekt erstellt, einer Funktion
übergeben und ausgegeben wird.

\begin{lstlisting}[frame=single]
void func(QTime time) {
	qDebug() << time.toString();
}

QTime time(1,2);
func(time);
\end{lstlisting}

QTime selbst ist eine Klasse welche nicht nur Stunden, Minuten, Sekunden
und Millisekunden speichern kann. Sondern auch die Textausgabe im
jeweiligen Landesformat übernimmt und mit der Klasse QDateTime werden
auch Winter und Sommerzeit beachtet.

\section{Technische Details des Fehlers}
Wie am Anfang erwähnt ist diese Fehlerart unabhängig der gewählten
Programmiersprache. Auch in C können Arrays als Funktions Parameter
eine feste Länge zugewiesen werden. Und da C++ rückwärts kompatibel
zu C ist, ist der Fehler auch in C++ machbar. Es hängt nicht von der 
Sprache ab, sondern die Art wie man programmiert. \\
In der Subroutine wurde ein Array fester Länge angegeben. Damit 
existiert das Array für den Compiler! und er würde so nie einen
Fehler finden. Warum die ganzen Memory Check Programm auch keinen
Fehler finden, ist recht einfach. Es existiert im Programm ja auch
ein Array richtiger Länge. Es wurde nur ein statt zwei Elemente 
übergeben. Aber in dem Moment wo in der Subroutine auf das
zweite Element zugegriffen wird, wird gültiger Speicher ausgelesen.

\end{document}


No Comments

No comments yet.

RSS feed for comments on this post.

Sorry, the comment form is closed at this time.

Powered by WordPress