Тест производительности кода Java и FPC на Anrdoid'е | 
			10.03.2014 Александр Савиных  | 
			
Я провёл несколько тестов с целью сравнить производительность кода на Java и FPC на операционной системе Android.
В этом документе я описываю какие результаты мне удалось получить и также некоторые подробности о том, как именно тестировал производительность и как получил эти результаты.
Вот что мне удалось найти: http://blog.naver.com/simonsayz/120196955980.
Некий Yi Xian проверяет производительность... пустого цикла. Ну почти.
Вот часть скриншота из той записи:
Здесь у автора получилось, что код на FreePascal выполняет этот цикл за 3 секунды, а код на Java выполняет его за 3.4 секунды. Я посчитал такой тест слишком поверхностным, и поэтому решил протестировать производительность более масштабно.
Я решил тестировать перемножение матриц. Можно было бы генерировать матрицы случайным образом и в коде на Java, и в коде для FPC, однако я решил сделать так, чтобы использовались заранее подготовленные данные. Таким образом я исключил влияние рандома на результаты тестов. Чтобы генерировать тестовые данные, я создал небольшое приложение для Windows; файл проекта: AnWoSp\PJBench\pas\PJMatrixGenPro.lpi (в конце статьи будет ссылка на архив с проектом).
Вот какого вида XML-файл создаёт вспомогательное приложение:
<?xml version="1.0" encoding="utf-8"?>
<matrixList>
  <matrix width="3" height="3">
    <column>
      <cell>-17</cell>
      <cell>59</cell>
      <cell>-98</cell>
    </column>
    <column>
      <cell>-47</cell>
      <cell>-90</cell>
  ... ... ...
Корневой элемент 'matrixList' (список матриц), в нём элементы 'matrix' (матрица), в них в свою очередь элементы 'column' (колонка) и 'cell' (ячейка). В финальном варианте теста было 100 матриц размером 10x10. Каждая ячейка матрицы содержит целое число от -100000 до +100000. Этот файл я назвал data.xml. После того как файл сгенерирован с помощью программы PJMatrixGenPro, его нужно положить в подпапку assets папки с Android-проектом Eclipse ADT.
Для тестового приложения, которое должно было работать на Андроиде я не использовал Android Module Wizard, а так же я не использовал LCL. Для работы мне понадобилось вот что:
Здесь я не описываю подробно как скомпилировать кросскомпилятор и настроить его чтобы компилировать нативные библиотеки для Android-ARM, так как статьи на эту тему уже есть. Для начала можно посмотреть здесь: http://wiki.lazarus.freepascal.org/Android
Я организовал взаимодействие между кодом на Java и кодом на FPC с помощью Java Native Interface. Файл проекта размещён в подпапке pas проекта Eclipse: AnWoSp\PJBench\pas\PJBenchPro.lpi. Проект Eclipse это папка AnWoSp\PJBench, а папка AnWoSp это моё рабочее пространство Eclipse. (Существует такое понятие "рабочее пространство" в Eclipse, обозначает папку с проектами).
Вот какой код можно увидеть в главном файле тестового приложения PJBenchPro.lpr:
procedure SetPackagePath(aEnv: PJNIEnv; aThis: jobject; aJavaString: jstring); cdecl;
...
procedure Test(aEnv: PJNIEnv; aThis: jobject); cdecl;
...
begin
  RegisterProc('SetPackagePath', '(Ljava/lang/String;)V', @SetPackagePath);
  RegisterProc('Test', '()V', @Test);
Это позволяет вызывать методы динамической библиотеки из Java; вот как они объявляются в Java:
public class MainActivity extends Activity {
	protected native void SetPackagePath(String filePath);
	protected native void Test();
Таким образом из Java можно вызывать эти методы.
Сборка и запуск проекта осуществляется следующим образом:
Про этот метод сборки приложений на Android и про то как использовать JNI для организации взаимодействия кода на Java и FPC я узнал изучая код библиотеки ZenGL. Там же есть пример вызова методов Java из FPC (для тестового проекта мне это не понадобилось, в нём я только вызываю FPC-процедуры из Java).
Вот как у меня загружается XML-документ с тестовыми данными в java:
protected Document loadTestDocument() throws Exception {
	long time = getNanoTime();
	AssetManager assetManager = getAssets();
	InputStream input = assetManager.open("data.xml");
	DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
	Document doc = builder.parse(input);
	time = System.nanoTime() - time;
	WriteLog("XML Document loaded; time spent: " + ((double)time * nsts) + " seconds");
	WriteLog("Matrices in list: " + doc.getFirstChild().getChildNodes().getLength() + " items");
	return doc;
}
Файл открывается вызовом assetManager.open("data.xml"). Здесь указывают имя файла, который положили ранее в папку assets.
Вот сколько времени это заняло: 0,877 секунд. Здесь и далее везде единицами измерения времени у меня будут секунды.
Вот что ещё важно отметить по поводу Java: как правило, при первом запуске теста всё происходит медленнее, чем на втором и последующем запусках. Это связано с особенностью Java-машины, которая кэширует код, ну и на следующих запусках она запускает уже кэшированный код, а не загружает его опять. Так же там есть какие-то оптимизации, а так же псевдослучайные факторы (на системе работают всякие фоновые процессы), так что результат измерения времени получается всегда разный как для кода на Java, так и для кода на FPC, хотя для Java разброс по времени значительно больше.
В Java для измерения времени я использовал функцию System.nanoTime(). nsts это множитель для первода наносекунд в секунды, который равер 10^9 = 1000000000.
	public final double nanoSecondsToSeconds = (double)1 / (double)1000000000;
	protected final double nsts = nanoSecondsToSeconds;
А теперь загрузку XML-документа с тестовыми данными в коде на FreePascal:
function Load: TIntegerMatrixArray;
var
  stream: TStream;
  t: TimerData;
  doc: TXMLDocument;
begin
  WriteLog('Now unpacking data...');
  ClearStart(t);
  stream :=  CreateStream(PackageFilePath, 'assets/data.xml');
  Stop(t);
  WriteLog('Got data: ' + IntToStr(stream.Size) + ' bytes; time spend: ' + GetElapsedStr(t));
  ClearStart(t);
  stream.Position := 0;
  ReadXMLFile(doc, stream);
  stream.Free;
  WriteLog('Pharsed XML data; time spent: ' + GetElapsedStr(t));
  WriteLog('Matrices in list: ' + IntToStr(doc.FirstChild.ChildNodes.Count) + ' items');
  result := LoadMatrixArray(doc);
  doc.Free;
end;
PackageFilePath это путь к архиву приложения, который устанавливается в системе. Этот путь код на Java передаёт в FPC-библиотеку с помощью вызова SetPackagePath (который зарегистрирован в JNI, как описано выше). У меня этот путь: /data/app/hinst.pjbench-2.apk. Этот файл является zip-архивом. Функция CreateStream извлекает файл из zip-архива в память:
uses 
  zipper,
...
function CreateStream(const aFilePath: string; const aSubFilePath: String): TStream;
var
  h: THelper;
  z: TUnZipper;
  strings: TStrings;
begin
  z := TUnZipper.Create;
  z.FileName := aFilePath;
  h := THelper.Create;
  z.OnCreateStream := @h.CreateStream;
  z.OnDoneStream := @h.DoneStream;
  strings := TStringList.Create;
  strings.Add(aSubFilePath);
  z.UnZipFiles(strings);
  result := h.Result;
  strings.Free;
  h.Free;
  z.Free;
end;
После этого распакованные данные загружаются в XML-документ: ReadXMLFile(doc, stream);
Таким образом код на FreePascal делает то же самое, что и код на Java: загружает XML-документ. Для паскаля мне удалось разбить этот процесс на 2 этапа: распаковка и парсинг XML. В Java у меня это происходит в один этап потому, что AssetManager скрывает от программиста процесс распаковки данных, и мы не знаем как он там происходит. . Скорее всего, однажды распакованный ресурс кэшируется на время работы программы, так что можно сказать что коду на Java в этой задаче в некотором смысле облегчили работу, ведь код на FPC распаковывает архив каждый раз. (На самом деле я не заметил чтобы data.xml кэшировался: разница между первым и последующим запусками java-теста для этой задачи была очень маленькая).
В результате выяснилось, что код на FreePascal справляется с задачей намного быстрее. На распаковку уходит 0,0181 секунд, а на разбор XML-текста 0,0483 секунд.
Итак, задача "распаковка и парсинг XML":
Java: 0.877 секунд FPC: 0.0664 секунд // 0.0664 = 0.0181 + 0.0483
Дополнение: на самом деле при желании всё таки можно было сделать, чтобы в коде на Java сначала текст XML-документа полностью загружался в память, а потом происходил парсинг, но я не стал этого делать.
Вот как выглядит моё тестовое приложение на экране мобильного телефона. Возможно запустить само приложение один раз и запустить тесты несколько раз. Таким образом когда я говорю, что я запускал тесты для Java несколько раз подряд, то я имею в виду, что запускал их не перезапуская всего приложения, то есть, они работали в одном и том же экземпляре Java-машины, что давало ей возможность кэшировать код. В таблицу результатов я заносил среднее значение времени по второму и последующим запускам.
Переходим к следующей задаче: загрузка данных из XML-документа. Когда я перемножал матрицы, я брал данные не из прямо из XML-структуры, а предварительно извлекал из XML-структуры матрицы. В Java матрицами были int[][], то есть, двумерные массивы int. Список матриц: int[][][]
Ниже представлен код загрузки матриц на Java, который использовался для тестирования:
protected int[][] loadMatrix(Node node) {
	int width = Integer.parseInt(node.getAttributes().getNamedItem("width").getTextContent());
	int height = Integer.parseInt(node.getAttributes().getNamedItem("width").getTextContent());
	int[][] matrix = new int[width][height];
	Node column = node.getFirstChild();
	int x = 0;
	while (column != null) {
		if (column.getTextContent().trim().length() > 0) {
			Node cell = column.getFirstChild();
			int y = 0;
			while (cell != null) {
				if (cell.getTextContent().trim().length() > 0) {
					matrix[x][y] = Integer.parseInt(cell.getTextContent());
					y++;
				}
				cell = cell.getNextSibling();
			}
			++x;
		}
		column = column.getNextSibling();
	}
	return matrix;
}
protected int[][][] loadMatrixArray(Document doc) throws Exception {
	long time = getNanoTime();
	Node node = doc.getFirstChild().getFirstChild();
	List matrixList = new LinkedList(); 
	while (node != null) {
		if (node.getTextContent().trim().length() > 0) {
			int[][] matrix = loadMatrix(node);
			matrixList.add(matrix);
		}
		node = node.getNextSibling();
	}
	int[][][] result =  matrixList.toArray(new int[0][][]);
	time = System.nanoTime() - time;
	WriteLog("Load matrix array from xml: time spent: " + (nsts * time) + " secs");
	WriteLog("Items in array: " + result.length);
	return result;
}
  
Обратите внимание на код: if (node.getTextContent().trim().length() > 0). Он нужен потому, что Java в отличие от FPC при разборе XML-документа по умолчанию включает в структуру пробелы и переносы строк тоже. Так что, получается много "пустых" узлов. Возможно, такое поведение прописано в каком-нибудь стандарте. Могу предположить, что сохранять узлы-пробелы нужно для того, чтобы по экземпляру Document можно было полностью восстановить точную копию исходного текста, в то время как в FPC информация о том, как были расставлены пробелы и переносы строк теряется (если только не сохраняется где-то скрыто, о чём я не знаю).
А теперь код для загрузки матриц из TXMLDocument'а на FPC:
function LoadMatrixArray(const aDoc: TXMLDocument): TIntegerMatrixArray;
var
  timer: TimerData;
  node: TDOMNode;
  width, height: Integer;
  matrix: TIntegerMatrix;
  x, y, i: Integer;
begin
  ClearStart(timer);
  SetLength(result, aDoc.FirstChild.ChildNodes.Count);
  node := aDoc.FirstChild.FirstChild;
  i := 0;
  while node <> nil do
  begin
    width := StrToInt(node.Attributes.GetNamedItem('width').TextContent);
    height := StrToInt(node.Attributes.GetNamedItem('height').TextContent);
    SetLength(matrix, width, height);
    for x := 0 to width - 1 do
      for y := 0 to height - 1 do
        matrix[x, y] := StrToInt(node.ChildNodes[x].ChildNodes[y].TextContent);
    node := node.NextSibling;
    result[i] := matrix;
    Inc(i);
  end;
  Stop(timer);
  WriteLog('Load matrix list from xml: time spent: ' + GetElapsedStr(timer));
end;
Этот код подготавливает данные в виде массива матриц: TIntegerMatrixArray = array of TIntegerMatrix; ну а сама матрица это в свою очередь двумерный массив целых чисел: TIntegerMatrix = array of array of Integer;
Я старался писать код для Java и для FPC как можно более похоже. Можно заметить как исходные коды на FPC и Java сильно напоминают друг друга и делают, в сущности, одно и то же, однако некоторых отличий мне всё же избежать не удалось.
Вот результаты теста производительности для задачи загрузки матриц из XML-структуры:
Java: 0.824 сек FPC: 0.0223 сек
Код на FreePascal извлекает массив матриц из XML-структуры намного быстрее, чем код на Java.
Ну а теперь переходим к в некотором смысле основной задаче, ради которой всё и затевалось: перемножение матриц. Для этой задачи коды на Java и FreePascal получились очень похожими, практически идентичными:
Java:
// calc product of square matrices
protected int[][] prodSM(int[][] a, int[][] b) {
	int w = a.length;
	int[][] c = new int[w][w];
	for (int x = 0; x < w; ++x) {
		for (int y = 0; y < w; ++y) {
			int cellValue = 0;
			for (int r = 0; i < w; ++i)
				cellValue = cellValue + a[r][y] * b[x][r];
			c[x][y] = cellValue;
		}
	}
	return c;
}
FreePascal:
function prodSM(const a, b: TIntegerMatrix): TIntegerMatrix;
var
  x, y, w: Integer;
  c: TIntegerMatrix;
  cellValue, r: Integer;
begin
  w := Length(a);
  SetLength(c, w, w);
  for x := 0 to w - 1 do
  begin
    for y := 0 to w - 1 do
    begin
      cellValue := 0;
      for r := 0 to w - 1 do
        cellValue := cellValue + a[r, y] * b[x, r];
      c[x, y] := cellValue;
    end;
  end;
  result := c;
end;
Эти функции вычисляют произведение двух квадратных матриц. А у меня в массиве тестовых данных 100 матриц, и вот как я решил с целью теста перемножить их между собой:
Java:
protected int[][][] bench(int[][][] matrixArray) {
	long time = getNanoTime();
	int n = matrixArray.length;
	int[][][] resultArray = new int[n][][];
	for (int i = 0; i < n; ++i)
		resultArray[i] = prodSM(matrixArray[i], matrixArray[n - i - 1]);
	time = getNanoTime() - time;
	WriteLog("matrix products calculated; time spent: " + (nsts * time) + " secs");
	return resultArray;
}
FreePascal:
function Bench(const a: TIntegerMatrixArray): TIntegerMatrixArray;
var
  n, i, w: Integer;
  time: TimerData;
begin
  ClearStart(time);
  n := Length(a);
  SetLength(result, n);
  for i := 0 to n - 1 do
    result[i] := prodSM(a[i], a[n - i - 1]);
  Stop(time);
  WriteLog('matrix products calculated; time spent: ' + GetElapsedStr(time) + ' secs');
end;
Обратите внимание на код  result[i] := prodSM(a[i], a[n - i - 1]); Можно это изобразить как-то так:
Первая матрица умножается на последнюю, вторая матрица умножается на предпоследнюю, и так далее, а в конце последняя матрица умножается на первую. Делается это совершенно одинаковым образом в коде на Java и в коде на FPC. Матрицы-результаты умножения сохраняются в отдельный массив, длина которого совпадает с количеством исходных матриц.
Результаты теста:
Java: 0.00625 секунд FPC: 0.00389 секунд
В этой конкретной задаче Java показывает самую большую разницу между первым и последующим запуском: при первом запуске код на Java затрачивает на вычисление произведения матриц 0.01 секунд, а во всех последующих запусках 0.006 секунд. Поэтому в диаграмме я разместил две полосы для Java: для первого запуска и для последующих запусков.
Быстрее всего работает код на FPC, ощутимо медленнее работает код на Java, и ещё медленнее работает код на Java при первом запуске. Ну и специально на случай если кому-то этот результат покажется недостаточно впечатляющим, я кроме того провёл тест с включённой оптимизацией третьего уровня -O3 в FPC (верхняя строка на диаграмме), и для данной вычислительной задачи включение оптимизации дало существенный прирост производительности. Для всех остальных задач после включения максимальной оптимизации результат почти не изменился из-за того, что большая часть вызывающегося для них кода для работы с XML и zip вызывается из RTL, так что мне пришлось бы перекомпилировать FPC RTL, чтобы получить значительный эффект. Можно было перекомпилировать RTL с оптимизацией, но я посчитал это ненужным, так как в остальных задачах FPC и так работает намного быстрее
Первоначально этого раздела вообще не предполагалось, но я решил всё таки сделать дополнительную проверку, чтобы убедиться, что код работает правильно. Как оказалось, не зря: в коде на FPC у меня была одна незамеченная ранее дурацкая ошибка, из-за чего код на FPC перемножал матрицы размером 0 на 0. Это сильно искажало результаты тестов. Однако, благодаря проверке, которая описана в этом разделе, мне удалось исправить эту ошибку. В этой статье (всюду, в том числе и выше) приведены результаты уже с учётом всех корректировок, результаты с ошибкой я полностью заменил и перепроверил всё ещё несколько раз.
Для того, чтобы проверить правильность вычисляемых результатов, я создал код для сохранения произведений матриц в XML-файл, за одно и протестировал его производительность.
Вот код сохранения результатов на Java: (ничего необычного в нём нет: он сохраняет данные в XML-файл всё в том же формате: matrixList/matrix/column/cell, поэтому можно его просто пролистать без ущерба для понимания смысла статьи).
protected Document matrixArrayToDocument(int[][][] array) throws Exception {
	long time = getNanoTime();
	DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
	Document doc = builder.newDocument();
	Node matrixListNode = doc.createElement("matrixList");
	for (int i = 0; i < array.length; ++i) {
		Node matrixNode = doc.createElement("matrix");
		Node widthAttr = doc.createAttribute("width");
		widthAttr.setTextContent("" + array[i].length);
		matrixNode.getAttributes().setNamedItem(widthAttr);
		Node heightAttr = doc.createAttribute("height");
		heightAttr.setTextContent("" + array[i].length);
		matrixNode.getAttributes().setNamedItem(heightAttr);
		for (int x = 0; x < array[i].length; ++x) {
			Node column = doc.createElement("column");
			for (int y = 0; y < array[i].length; ++y) {
				Node cell = doc.createElement("cell");
				cell.setTextContent("" + array[i][x][y]);
				column.appendChild(cell);
			}
			matrixNode.appendChild(column);
		}
		matrixListNode.appendChild(matrixNode);
	}
	doc.appendChild(matrixListNode);	
	time = getNanoTime() - time;
	WriteLog("Save matrix array to xml document: " + (nsts * time) + " seconds");
	return doc;
}
protected void saveDocumentToFile(Document doc, String filePath) throws Exception {
	long time = getNanoTime();
	Transformer transformer = TransformerFactory.newInstance().newTransformer();
	StreamResult streamResult = new StreamResult(new StringWriter());
	DOMSource domSource = new DOMSource(doc);
	transformer.transform(domSource, streamResult);
	String xmlString = streamResult.getWriter().toString();
	BufferedWriter bufferedWriter = new BufferedWriter(
	  new OutputStreamWriter(new FileOutputStream(new File(filePath))));
	bufferedWriter.write(xmlString);
	bufferedWriter.flush();
	bufferedWriter.close();				
	time = getNanoTime() - time;
	WriteLog("Save xml document to file: " + (nsts * time) + " seconds");
}
protected void save(int[][][] array, String filePath) throws Exception {
	Document doc = matrixArrayToDocument(array);
	saveDocumentToFile(doc, filePath);
}
Процедура saveDocumentToFile получилась несколько сложнее, чем могла бы быть. Это произошло из-за того, что я хотел удостовериться, что данные полностью записываются на диск к моменту возврата из метода, иначе получилось бы "не честно". В первоначальном варианте saveDocumentToFile в качестве аргумента конструктора для StreamResult передавался экземпляр File, но в таком способе я не нашёл способа вызвать метод close или flush, поэтому получалась "отложенная" запись.
А вот код сохранения данных на FreePascal:
function CreateElement(aDocument: TXMLDocument; a: TIntegerMatrix): TDOMElement;
var
  x, y, width, height: Integer;
  column, cell: TDOMElement;
begin
  result := aDocument.CreateElement('matrix');
  width := Length(a);
  if width <> 0 then
    height := Length(a[0])
  else
    height := 0;
  result.SetAttribute('height', IntToStr(height));
  result.SetAttribute('width', IntToStr(width));
  for x := 0 to Length(a) - 1 do
  begin
    column := aDocument.CreateElement('column');
    for y := 0 to Length(a[x]) - 1 do
    begin
      cell := aDocument.CreateElement('cell');
      cell.TextContent := IntToStr(a[x, y]);
      column.AppendChild(cell);
    end;
    result.AppendChild(column);
  end;
end;
function CreateDocument(const a: TIntegerMatrixArray): TXMLDocument;
var
  i: Integer;
  matrixList: TDOMElement;
begin
  result := TXMLDocument.Create;
  matrixList := result.CreateElement('matrixList');
  for i := 0 to Length(a) - 1 do
  begin
    matrixList.AppendChild(CreateElement(result, a[i]));
  end;
  result.AppendChild(matrixList);
end;
procedure Save(const a: TIntegerMatrixArray; const aFilePath: string);
var
  doc: TXMLDocument;
  time: TimerData;
begin
  ClearStart(time);
  doc := CreateDocument(a);
  Stop(time);
  WriteLog('Save matrix array to xml document: ' + GetElapsedStr(time) + ' seconds');
  ClearStart(time);
  WriteXML(doc, aFilePath);
  Stop(time);
  WriteLog('Save xml document to file: ' + GetElapsedStr(time) + ' seconds');
  doc.Free;
end;
Функция CreateElement используется и во "вспомогательном" приложении, которое подготавливает тестовые данные и работает под Windows.
XML-файл с произведениями матриц сохранялся на SD-карту телефона в '/mnt/sdcard'. Я создаю на карте памяти два отдельных файла: с результатами работы кода на Java и с результатами работы кода на FreePascal. Вот начало содержимого результирующего файла от кода на FreePascal:
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<matrixList>
  <matrix width="3" height="3">
    <column>
      <cell>3888</cell>
      <cell>4741</cell>
      <cell>-3595</cell>
    </column>
    <column>
      <cell>11324</cell>
      <cell>6706</cell>
      <cell>666</cell>
    </column>
    <column>
      <cell>-6925</cell>
      <cell>-10826</cell>
      <cell>7321</cell>
    </column>
  </matrix>
  <matrix width="10" height="10">
    <column>
      <cell>-666166708</cell>
      <cell>-1374090177</cell>
      <cell>1324272656</cell>
      <cell>475097438</cell>
      <cell>1168412725</cell>
      <cell>-935531146</cell>
	  ... ... ...
После того, как я исправил у себя все ошибки, содержимое файла с результатами работы кода на Java полностью совпадает с содержимым файла с результатами работы кода на FPC с одним небольшим отличием: код на Java не расставляет пробелы и переносы строк, то есть, не отформатирован, так что мне пришлось отформатировать его перед проверкой. При желании можно было бы попробовать сделать, чтобы файл был сразу отформатированным.
Можно заметить, что в самом начале матрица размером 3 на 3, а не 100 на 100, однако это не должно вводить в заблуждение. Для того, чтобы сделать ручную проверку результата перемножения матриц, я прибег к некоторому "трюку": в самом начале и в самом конце тестовых данных я поместил по одной матрице 3х3, а между ними, как и задумывалось, 100 матриц размером 10х10. Я сделал это специально чтобы можно было легче проверить правильность первого результата:
Код, использованный для создания тестовых данных:
// 3x3, 10x10, 10x10, 10x10, ... всего 100 раз ..., 10x10, 10x10, 10x10, 3x3
const
  MatrixCount = 100;
  
procedure GenerateTestingData;
var
  i: Integer;
  width, height: Integer;
  matrix: TIntegerMatrix;
  matrixElement: TDOMElement;
  matrixListElement: TDOMElement;
  doc: TXMLDocument;
begin
  WriteLn('Now generating testing data...');
  doc := TXMLDocument.Create;
  matrixListElement := doc.CreateElement('matrixList');
  matrixListElement.AppendChild(CreateElement(doc, CreateRandomMatrix(3, 3, -100, 100)));
  width := 10;
  height := 10;
  for i := 0 to MatrixCount - 1 do
  begin
    WriteLn('Matrix #' + IntToStr(i) + '...');
    matrix := CreateRandomMatrix(width, height, -100000, 100000);
    matrixElement := CreateElement(doc, matrix);
    matrixListElement.AppendChild(matrixElement);
  end;
  matrixListElement.AppendChild(CreateElement(doc, CreateRandomMatrix(3, 3, -100, 100)));
  doc.AppendChild(matrixListElement);
  WriteXMLFile(doc, 'data.xml');
  doc.Free;
end;
Так же можно заметить, что значения "тестовых" матриц берутся в отрезке от -100 до 100. Это тоже сделано чтобы можно было проще проверить правильность перемножения.
В конце я сделал проверку следующим образом: нашёл онлайн-калькулятор для умножения матриц и ввёл в него значения первой и последней матриц из файла исходных данных data.xml:
Первая и последняя матрицы, которые в соответствии с тестовым алгоритмом будут перемножены:
 ... 
Сравнение результата вычислений онлайн-калькулятора и результирующего файла:
 ... 
... 
Результирующая матрица, полученная на онлайн-калькуляторе совпадает с матрицей из файла с результатами. Совпадают результаты и для FPC, и для Java, на изображении выше показан файл от кода на FPC.
А вот сравнение производительности Java и FPC на задаче сохранения результатов в XML-файл:
Java: 0.378 сек FPC: 0.0505 сек
Java: 0.518 сек FPC: 0.0307 сек
Здесь на первом этапе (синим) происходит создание XML-структуры на основе двумерных массивов целых чисел, а на втором этапе (оранжевым) полученная XML-структура записывается в XML-файл на карте памяти. Код для этих действий на Java и на FreePascal приведён выше в этом разделе.
Можно заметить, что в задаче сохранения XML-структуры в файл на карте памяти играет роль скорость записи на карту памяти, которая в некотором смысле мало зависит от того, сделан ли запрос на запись из Java-машины или из нативной библиотеки, тем не менее код на FreePascal справляется и с этой задачей намного быстрее, чем код на Java. Вероятно, причина кроется в том, что код на Java обращается к нативным Linux-библиотекам, ответственным за файловую систему, через промежуточные слои API в то время как код на FPC обращается к ним более напрямую, к тому же в последней задаче перед записью данных в файл происходит преобразование XML-структуры в текст.
Кроме файлов r.java.xml и r.fpc.txt с результатами вычислений тестовое приложение создаёт в корневом каталоге карты памяти телефона файлы fpc.log.txt и log.java.txt, в которые дублируется весь отладочный вывод для того, чтобы можно было посмотреть результаты работы приложения без подключения сборщика лога для Android. Для того, чтобы текст, записанный в log.java.txt, оказался на диске, следует завершить приложение с помощью кнопки Exit в меню приложения либо с помощью жеста перетаскивания в сторону в списке недавних приложений, появляющемся при долгом нажатии кнопки "дом".
Пример отладочного вывода от FPC-части приложения:
FPC Dynamic library initialization JNI_OnLoad Class found: True Now registering methods: 2 Register natives success: True BEF7D608 Package path = "/data/app/hinst.pjbench-1.apk" Now starting test.. Now unpacking data... Got data: 298473 bytes; time spend: 0.032164 Pharsed XML data; time spent: 0.051405 Matrices in list: 102 items Load matrix list from xml: time spent: 0.021670 matrix products calculated; time spent: 0.001733 secs Save matrix array to xml document: 0.050900 seconds Save xml document to file: 0.036277 seconds empty cycle; time spent: 0.241562 secs Now starting test.. Now unpacking data... Got data: 298473 bytes; time spend: 0.017942 Pharsed XML data; time spent: 0.048435 Matrices in list: 102 items Load matrix list from xml: time spent: 0.021526 matrix products calculated; time spent: 0.001862 secs Save matrix array to xml document: 0.050598 seconds Save xml document to file: 0.030397 seconds empty cycle; time spent: 0.200201 secs
Пример отладочного вывода от Java-части приложения:
XML Document loaded; time spent: 1.110052231 seconds Matrices in list: 205 items Load matrix array from xml: time spent: 1.3849909230000002 secs Items in array: 102 matrix products calculated; time spent: 0.012251077 secs empty cycle; time spent: 0.615418231 secs Save matrix array to xml document: 0.43670400000000004 seconds Save xml document to file: 2.808533231 seconds XML Document loaded; time spent: 0.873298385 seconds Matrices in list: 205 items Load matrix array from xml: time spent: 0.8605713850000001 secs Items in array: 102 matrix products calculated; time spent: 0.018320692 secs empty cycle; time spent: 0.616836308 secs Save matrix array to xml document: 0.44513115400000003 seconds Save xml document to file: 0.737596539 seconds
Вот суммарное время, потраченное на все задачи: распаковка и загрузка данных, вычисление произведений матриц и сохранение результатов в XML-файл:
Java: 2.60325 сек FPC: 0.17379 сек
На этом графике видно, что собственно перемножение матриц заняло очень мало времени в сравнении с другими задачами: в полоске для Java зелёной части между Load и Save почти не видно, то же самое верно и для FPC. Тем не менее я считаю сравнение скорости перемножения матриц на Java и на FreePascal значимым.
Однажды я читал в одной статье как автор выражал недовольство тем, что для тестирования производительности измеряется время выполнения пустого цикла for. Я решил провести и такой тест.
Код на Java:
protected void emptyCycleBench() {
	long time = getNanoTime();
	for (int i = 0; i < 100000000; i++) 
		;
	time = getNanoTime() - time;
	WriteLog("empty cycle; time spent: " + (nsts * time) + " secs");
}	
Код на FreePascal:
procedure EmptyCycleBench;
var
  i, j: Integer;
  time: TimerData;
begin
  ClearStart(time);
  for i := 0 to 100000000 do
    ;
  Stop(time);
  WriteLog('empty cycle; time spent: ' + GetElapsedStr(time) + ' secs');
end;
И вот результат:
Java: 0.5 сек FPC: 0.8 сек
Не знаю почему, но выполнение пустого цикла - единственная задача, с которой код на Java справился быстрее, чем код на FPC (среди рассмотренных мною тестовых задач). Однако FPC с включённой оптимизацией третьего уровня всё-таки выполняет и эту задачу быстрее.
Я положил в архив всё необходимое для сборки моего тестового проекта:
Всё кроме модулей которые писал не я (EpikTimer и интерфейсы к Android NDK) я предоставляю по лицензии Modified LGPL с исключением для статической линковки (эта лицензия используется в многих библиотеках для FPC, в том числе в FPC RTL & FCL). Я заметил, что Lazarus 1.2 RC 2 работает с кодом проекта лучше всего, в то время как в Lazarus 1.0.x возникают ошибки при разборе исходного кода RTL, из-за которых не работает автодополнение.
В Lite-версии проекта я удалил всё лишнее, что только было можно. Больше всего занимает служебная папка .metadata Eclipse, однако её удалять нельзя, иначе проект не будет строиться.
Я думаю, что проведённые мною тесты убедительно показывают, что нативный код на FreePascal не только работает значительно быстрее, чем аналогичный код на Java, но и может быть использован для решения практических задач, для написания приложений, требующих высокой производительности
Для организации взаимодействия кода на Java и FPC мне потребовалось приложить самые минимальные усилия. В то же время можно с лёгкостью разработать пользовательский интерфейс для Android-приложения полностью на Java, как это и сделано в данном примере.
Кроме того, позволю предположить себе следующее: Java-машина по каким-то причинам "любит" простые конструкции: пустые циклы, которые она выполняет даже быстрее, чем FPC. То же самое, скорее всего, справедливо и для перемножения матриц: несколько вложенных циклов, которые обращаются к элементам массивов это достаточно простая программная конструкция, для которой Java-машина, скорее всего, каким-то образом полностью кэширует инструкции, а вот по мере возрастания количества вложенных вызовов и создания большого количества экземпляров классов производительность Java-кода падает. Что и наблюдалось в ходе теста: при перемножении матриц Java проигрывает совсем немного, а вот при работе с XML, где наверняка происходит множество вложенных вызовов, создаются экземпляры классов и выделяется память, Java проигрывает почти в 10 раз. Таким образом, если бы я решил провести какой-нибудь достаточно сложный расчёт с помощью модульной вычислительной библиотеки, то, скорее всего, обнаружил бы, что код на FPC справился бы с этой задачей с значительно более большим отрывом. Но это - в следующий раз.
Ссылки:
| FPC | 3.2.2 | release | 
| Lazarus | 3.2 | release | 
| MSE | 5.10.0 | release | 
| fpGUI | 1.4.1 | release |