Тест производительности кода Java и FPC на Anrdoid'е

Я провёл несколько тестов с целью сравнить производительность кода на 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-файл создаёт вспомогательное приложение:

<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 несколько раз подряд, то я имею в виду, что запускал их не перезапуская всего приложения, то есть, они работали в одном и том же экземпляре Java-машины, что давало ей возможность кэшировать код. В таблицу результатов я заносил среднее значение времени по второму и последующим запускам.

Загрузка данных из XML-документа

Переходим к следующей задаче: загрузка данных из 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:



<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-файл:

Здесь на первом этапе (синим) происходит создание XML-структуры на основе двумерных массивов целых чисел, а на втором этапе (оранжевым) полученная XML-структура записывается в XML-файл на карте памяти. Код для этих действий на Java и на FreePascal приведён выше в этом разделе.

Можно заметить, что в задаче сохранения XML-структуры в файл на карте памяти играет роль скорость записи на карту памяти, которая в некотором смысле мало зависит от того, сделан ли запрос на запись из Java-машины или из нативной библиотеки, тем не менее код на FreePascal справляется и с этой задачей намного быстрее, чем код на Java. Вероятно, причина кроется в том, что код на Java обращается к нативным Linux-библиотекам, ответственным за файловую систему, через промежуточные слои API в то время как код на FPC обращается к ним более напрямую, к тому же в последней задаче перед записью данных в файл происходит преобразование XML-структуры в текст.

Общее сравнение по всем задачам

Вот суммарное время, потраченное на все задачи: распаковка и загрузка данных, вычисление произведений матриц и сохранение результатов в XML-файл:

	Java: 2.60325  сек
	FPC:  0.17379  сек

На этом графике видно, что собственно перемножение матриц заняло очень мало времени в сравнении с другими задачами: в полоске для Java зелёной части между Load и Save почти не видно, то же самое верно и для FPC. Тем не менее я считаю сравнение скорости перемножения матриц на Java и на FreePascal значимым.

Пустой цикл for

Однажды я читал в одной статье как автор выражал недовольство тем, что для тестирования производительности измеряется время выполнения пустого цикла 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). Я заметил, что Lazarus 1.2 RC 2 работает с кодом проекта лучше всего, в то время как в Lazarus 1.0.x возникают ошибки при разборе исходного кода RTL, из-за которых не работает автодополнение.

Заключение

Я думаю, что проведённые мною тесты убедительно показывают, что нативный код на FreePascal не только работает значительно быстрее, чем аналогичный код на Java, но и может быть использован для решения практических задач, для написания приложений, требующих высокой производительности

Для организации взаимодействия кода на Java и FPC мне потребовалось приложить самые минимальные усилия. В то же время можно с лёгкостью разработать пользовательский интерфейс для Android-приложения полностью на Java, как это и сделано в данном примере.

Кроме того, позволю предположить себе вот что: Java-машина по каким-то причинам "любит" простые конструкции: пустые циклы, которые она выполняет даже быстрее, чем FPC. То же самое, скорее всего, справедливо и для перемножения матриц: несколько вложенных циклов, которые обращаются к элементам массивов это достаточно простая программная конструкция, для которой Java-машина, скорее всего, каким-то образом полностью кэширует инструкции, а вот по мере возрастания количества вложенных вызовов и создания большого количества экземпляров классов производительность Java-кода падает. Что и наблюдалось в ходе теста: при перемножении матриц Java проигрывает совсем немного, а вот при работе с XML, где наверняка происходит множество вложенных вызовов, создаются экземпляры классов и выделяется память, Java проигрывает почти в 10 раз. Таким образом, если бы я решил провести какой-нибудь достаточно сложный расчёт с помощью модульной вычислительной библиотеки, то, скорее всего, обнаружил бы, что код на FPC справился бы с этой задачей с значительно большим отрывом. Но это - в следующий раз.

Ссылки: