Я провёл несколько тестов с целью сравнить производительность кода на Java и FPC на операционной системе Android.
В этом документе я описываю какие результаты мне удалось получить и также некоторые подробности о том, как именно тестировал производительность и как получил эти результаты.
Я решил тестировать перемножение матриц. Можно было бы генерировать матрицы случайным образом и в коде на 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-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).
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[][][]
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(); ListmatrixList = 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 информация о том, как были расставлены пробелы и переносы строк теряется (если только не сохраняется где-то скрыто, о чём я не знаю).
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 сек
Ну а теперь переходим к в некотором смысле основной задаче, ради которой всё и затевалось: перемножение матриц. Для этой задачи коды на 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-файл:
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-структуры в текст.
Вот суммарное время, потраченное на все задачи: распаковка и загрузка данных, вычисление произведений матриц и сохранение результатов в 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 с включённой оптимизацией третьего уровня всё-таки выполняет и эту задачу быстрее.
Я думаю, что проведённые мною тесты убедительно показывают, что нативный код на FreePascal не только работает значительно быстрее, чем аналогичный код на Java, но и может быть использован для решения практических задач, для написания приложений, требующих высокой производительности
Для организации взаимодействия кода на Java и FPC мне потребовалось приложить самые минимальные усилия. В то же время можно с лёгкостью разработать пользовательский интерфейс для Android-приложения полностью на Java, как это и сделано в данном примере.
Кроме того, позволю предположить себе вот что: Java-машина по каким-то причинам "любит" простые конструкции: пустые циклы, которые она выполняет даже быстрее, чем FPC. То же самое, скорее всего, справедливо и для перемножения матриц: несколько вложенных циклов, которые обращаются к элементам массивов это достаточно простая программная конструкция, для которой Java-машина, скорее всего, каким-то образом полностью кэширует инструкции, а вот по мере возрастания количества вложенных вызовов и создания большого количества экземпляров классов производительность Java-кода падает. Что и наблюдалось в ходе теста: при перемножении матриц Java проигрывает совсем немного, а вот при работе с XML, где наверняка происходит множество вложенных вызовов, создаются экземпляры классов и выделяется память, Java проигрывает почти в 10 раз. Таким образом, если бы я решил провести какой-нибудь достаточно сложный расчёт с помощью модульной вычислительной библиотеки, то, скорее всего, обнаружил бы, что код на FPC справился бы с этой задачей с значительно большим отрывом. Но это - в следующий раз.
Ссылки: