From 5731feea253ce6ece973ef22b261428caa0301d3 Mon Sep 17 00:00:00 2001 From: golovin Date: Sun, 17 Dec 2023 07:56:19 +0300 Subject: [PATCH] 2023-06-30 --- .gitattributes | 2 + .gitignore | 3 + DIRECTORY-TREE.md | 61 ++++ README.en.md | 17 + README.md | 17 + build.sh | 51 +++ jekyll_site/Gemfile_color | 3 + jekyll_site/Gemfile_older | 3 + jekyll_site/_config_color.yml | 20 ++ jekyll_site/_config_older.yml | 20 ++ jekyll_site/_includes/counters_body.html | 2 + jekyll_site/_includes/counters_head.html | 16 + .../12/10/optimizing-matrix-multiplication.md | 248 +++++++++++++++ .../2021/12/13/matrix-rotation-90-degrees.md | 97 ++++++ .../2021/12/17/matrix-rotation-180-degrees.md | 85 +++++ .../matrix-multiplication-parallel-streams.md | 184 +++++++++++ .../2022/02/11/winograd-strassen-algorithm.md | 298 ++++++++++++++++++ jekyll_site/en/index.md | 76 +++++ jekyll_site/img/block-matrices.svg | 50 +++ jekyll_site/img/products.svg | 84 +++++ jekyll_site/img/sums1.svg | 125 ++++++++ jekyll_site/img/sums2.svg | 34 ++ jekyll_site/img/sums3.svg | 63 ++++ jekyll_site/robots.txt | 7 + .../12/09/optimizing-matrix-multiplication.md | 246 +++++++++++++++ .../2021/12/12/matrix-rotation-90-degrees.md | 96 ++++++ .../2021/12/16/matrix-rotation-180-degrees.md | 84 +++++ .../matrix-multiplication-parallel-streams.md | 182 +++++++++++ .../2022/02/10/winograd-strassen-algorithm.md | 293 +++++++++++++++++ jekyll_site/ru/index.md | 73 +++++ package.sh | 5 + serve.sh | 4 + 32 files changed, 2549 insertions(+) create mode 100644 DIRECTORY-TREE.md create mode 100644 README.en.md create mode 100644 README.md create mode 100755 build.sh create mode 100644 jekyll_site/Gemfile_color create mode 100644 jekyll_site/Gemfile_older create mode 100644 jekyll_site/_config_color.yml create mode 100644 jekyll_site/_config_older.yml create mode 100644 jekyll_site/_includes/counters_body.html create mode 100644 jekyll_site/_includes/counters_head.html create mode 100644 jekyll_site/en/2021/12/10/optimizing-matrix-multiplication.md create mode 100644 jekyll_site/en/2021/12/13/matrix-rotation-90-degrees.md create mode 100644 jekyll_site/en/2021/12/17/matrix-rotation-180-degrees.md create mode 100644 jekyll_site/en/2022/02/09/matrix-multiplication-parallel-streams.md create mode 100644 jekyll_site/en/2022/02/11/winograd-strassen-algorithm.md create mode 100644 jekyll_site/en/index.md create mode 100644 jekyll_site/img/block-matrices.svg create mode 100644 jekyll_site/img/products.svg create mode 100644 jekyll_site/img/sums1.svg create mode 100644 jekyll_site/img/sums2.svg create mode 100644 jekyll_site/img/sums3.svg create mode 100644 jekyll_site/robots.txt create mode 100644 jekyll_site/ru/2021/12/09/optimizing-matrix-multiplication.md create mode 100644 jekyll_site/ru/2021/12/12/matrix-rotation-90-degrees.md create mode 100644 jekyll_site/ru/2021/12/16/matrix-rotation-180-degrees.md create mode 100644 jekyll_site/ru/2022/02/08/matrix-multiplication-parallel-streams.md create mode 100644 jekyll_site/ru/2022/02/10/winograd-strassen-algorithm.md create mode 100644 jekyll_site/ru/index.md create mode 100755 package.sh create mode 100755 serve.sh diff --git a/.gitattributes b/.gitattributes index e69de29..4414986 100644 --- a/.gitattributes +++ b/.gitattributes @@ -0,0 +1,2 @@ +jekyll_site/ru/** linguist-language=Java +jekyll_site/en/** linguist-language=Java diff --git a/.gitignore b/.gitignore index c38fa4e..42e9bb7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ .idea *.iml +*.zip +_site* +.repo_*.sh diff --git a/DIRECTORY-TREE.md b/DIRECTORY-TREE.md new file mode 100644 index 0000000..ef0737b --- /dev/null +++ b/DIRECTORY-TREE.md @@ -0,0 +1,61 @@ +## Дерево каталогов + +
+.
+├─ jekyll_site
+│  ├─ _includes
+│  │  ├─ counters_body.html
+│  │  └─ counters_head.html
+│  ├─ en
+│  │  ├─ 2021
+│  │  │  └─ 12
+│  │  │     ├─ 10
+│  │  │     │  └─ optimizing-matrix-multiplication.md
+│  │  │     ├─ 13
+│  │  │     │  └─ matrix-rotation-90-degrees.md
+│  │  │     └─ 17
+│  │  │        └─ matrix-rotation-180-degrees.md
+│  │  ├─ 2022
+│  │  │  └─ 02
+│  │  │     ├─ 09
+│  │  │     │  └─ matrix-multiplication-parallel-streams.md
+│  │  │     └─ 11
+│  │  │        └─ winograd-strassen-algorithm.md
+│  │  └─ index.md
+│  ├─ img
+│  │  ├─ block-matrices.svg
+│  │  ├─ products.svg
+│  │  ├─ sums1.svg
+│  │  ├─ sums2.svg
+│  │  └─ sums3.svg
+│  ├─ ru
+│  │  ├─ 2021
+│  │  │  └─ 12
+│  │  │     ├─ 09
+│  │  │     │  └─ optimizing-matrix-multiplication.md
+│  │  │     ├─ 12
+│  │  │     │  └─ matrix-rotation-90-degrees.md
+│  │  │     └─ 16
+│  │  │        └─ matrix-rotation-180-degrees.md
+│  │  ├─ 2022
+│  │  │  └─ 02
+│  │  │     ├─ 08
+│  │  │     │  └─ matrix-multiplication-parallel-streams.md
+│  │  │     └─ 10
+│  │  │        └─ winograd-strassen-algorithm.md
+│  │  └─ index.md
+│  ├─ Gemfile_color
+│  ├─ Gemfile_older
+│  ├─ _config_color.yml
+│  ├─ _config_older.yml
+│  └─ robots.txt
+├─ CONTRIBUTING.md
+├─ DIRECTORY-TREE.md
+├─ LICENSE.md
+├─ OPEN_LICENSE.txt
+├─ README.en.md
+├─ README.md
+├─ build.sh
+├─ package.sh
+└─ serve.sh
+
diff --git a/README.en.md b/README.en.md new file mode 100644 index 0000000..205ffbb --- /dev/null +++ b/README.en.md @@ -0,0 +1,17 @@ +## Website pages + +- [Winograd — Strassen algorithm](https://pomodoro3.mircloud.ru/en/2022/02/11/winograd-strassen-algorithm.html) — 11.02.2022. +- [Matrix multiplication in parallel streams](https://pomodoro3.mircloud.ru/en/2022/02/09/matrix-multiplication-parallel-streams.html) — 09.02.2022. +- [Matrix rotation 180 degrees](https://pomodoro3.mircloud.ru/en/2021/12/17/matrix-rotation-180-degrees.html) — 17.12.2021. +- [Matrix rotation 90 degrees](https://pomodoro3.mircloud.ru/en/2021/12/13/matrix-rotation-90-degrees.html) — 13.12.2021. +- [Optimizing matrix multiplication](https://pomodoro3.mircloud.ru/en/2021/12/10/optimizing-matrix-multiplication.html) — 10.12.2021. + +## [Source texts](README.md) + +- Series of the static websites [«Pomodori»](https://hub.mos.ru/golovin.gg/pomodoro/blob/master/README.en.md). +- Used formats — Markdown, Liquid, YAML. +- Build tool — Jekyll with tomato design themes. +- Automation of processes — Bash scripts for command line. +- [build.sh](build.sh) — Building a site in two tomato themes and optimizing the results. +- [serve.sh](serve.sh) — Local deployment to verify the correctness of the build. +- [package.sh](package.sh) — Preparing an archive for subsequent deployment. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8ff27ff --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +## Страницы вёб-сайта + +- [Алгоритм Винограда — Штрассена](https://pomodoro3.mircloud.ru/ru/2022/02/10/winograd-strassen-algorithm.html) — 10.02.2022. +- [Умножение матриц в параллельных потоках](https://pomodoro3.mircloud.ru/ru/2022/02/08/matrix-multiplication-parallel-streams.html) — 08.02.2022. +- [Поворот матрицы на 180 градусов](https://pomodoro3.mircloud.ru/ru/2021/12/16/matrix-rotation-180-degrees.html) — 16.12.2021. +- [Поворот матрицы на 90 градусов](https://pomodoro3.mircloud.ru/ru/2021/12/12/matrix-rotation-90-degrees.html) — 12.12.2021. +- [Оптимизация умножения матриц](https://pomodoro3.mircloud.ru/ru/2021/12/09/optimizing-matrix-multiplication.html) — 09.12.2021. + +## [Исходные тексты](README.en.md) + +- Серия статических вёб-сайтов [«Помидоры»](https://hub.mos.ru/golovin.gg/pomodoro/blob/master/README.md). +- Используемые форматы — Markdown, Liquid, YAML. +- Инструмент сборки — Jekyll с помидорными темами оформления. +- Автоматизация процессов — Bash скрипты для командной строки. +- [build.sh](build.sh) — Сборка сайта в двух помидорных темах и оптимизация результатов. +- [serve.sh](serve.sh) — Локальное развёртывание для проверки корректности сборки. +- [package.sh](package.sh) — Подготовка архива для последующего развёртывания. diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..265203a --- /dev/null +++ b/build.sh @@ -0,0 +1,51 @@ +#!/bin/bash +echo "Сборка сайта в двух помидорных темах и оптимизация результатов." +milliseconds=$(date '+%s%3N') +rm -rf _site +rm -rf _site_older +rm -rf _site_color +echo "Сборка старого помидора." +mkdir -p _site_older +cp -r jekyll_site/_includes _site_older +cp -r jekyll_site/ru _site_older +cp -r jekyll_site/en _site_older +cp -r jekyll_site/ru/index.md _site_older +cp -r jekyll_site/_config_older.yml _site_older/_config.yml +cp -r jekyll_site/Gemfile_older _site_older/Gemfile +cd _site_older || exit +jekyll build +cp -r _site .. +cd .. +echo "Сборка цветного помидора." +mkdir -p _site_color +cp -r jekyll_site/_includes _site_color +cp -r jekyll_site/ru _site_color +cp -r jekyll_site/en _site_color +cp -r jekyll_site/ru/index.md _site_color +cp -r jekyll_site/_config_color.yml _site_color/_config.yml +cp -r jekyll_site/Gemfile_color _site_color/Gemfile +cd _site_color || exit +jekyll build +cp -r _site ../_site/color +cd .. +echo "Копирование без сборки." +cp -r jekyll_site/img _site +cp -r jekyll_site/robots.txt _site +echo "Оптимизация собранного контента." +cd _site || exit +cp -r assets/* . +rm -r assets +rm -r color/assets/favicon.ico +cp -r color/assets/* . +rm -r color/assets +rm -r color/404.html +find . -type f -name '*.html' | sort -r | while read -r file; do + sed -i 's/layout-padding=""/layout-padding/g' "$file" + sed -i 's/ class="language-plaintext highlighter-rouge"//g' "$file" + sed -i 's/ class="language-java highlighter-rouge"//g' "$file" + sed -i 's/
/
/g' "$file"
+  sed -i 's/<\/code><\/pre><\/div><\/div>/<\/code><\/pre><\/div>/g' "$file"
+  sed -i 's/
/
/g' "$file" + sed -i -r 's///g' "$file" +done +echo "Время выполнения сборки: $(("$(date '+%s%3N')" - "$milliseconds")) мс." diff --git a/jekyll_site/Gemfile_color b/jekyll_site/Gemfile_color new file mode 100644 index 0000000..136ccb7 --- /dev/null +++ b/jekyll_site/Gemfile_color @@ -0,0 +1,3 @@ +source "https://rubygems.org" +gem "jekyll" +gem "color-tomato-theme" diff --git a/jekyll_site/Gemfile_older b/jekyll_site/Gemfile_older new file mode 100644 index 0000000..5e19e6f --- /dev/null +++ b/jekyll_site/Gemfile_older @@ -0,0 +1,3 @@ +source "https://rubygems.org" +gem "jekyll" +gem "older-tomato-theme" diff --git a/jekyll_site/_config_color.yml b/jekyll_site/_config_color.yml new file mode 100644 index 0000000..8a6b449 --- /dev/null +++ b/jekyll_site/_config_color.yml @@ -0,0 +1,20 @@ +# site parameters +name: "Код с комментариями" +name_translated: "Code with comments" +url: "https://pomodoro3.mircloud.ru" +baseurl: "/color" +homepage_url: "https://git.org.ru/pomodoro/3" +homepage_name: "GIT.ORG.RU" +older_tomato_baseurl: "" +timezone: "Europe/Moscow" +author: "Головин Г.Г." +author_translated: "Golovin G.G." +translation_caption: "translation from Russian" +# build parameters +disable_disk_cache: true +theme: color-tomato-theme +defaults: + - scope: + path: "" + values: + layout: default diff --git a/jekyll_site/_config_older.yml b/jekyll_site/_config_older.yml new file mode 100644 index 0000000..b426c4f --- /dev/null +++ b/jekyll_site/_config_older.yml @@ -0,0 +1,20 @@ +# site parameters +name: "Код с комментариями" +name_translated: "Code with comments" +url: "https://pomodoro3.mircloud.ru" +baseurl: "" +homepage_url: "https://git.org.ru/pomodoro/3" +homepage_name: "GIT.ORG.RU" +color_tomato_baseurl: "/color" +timezone: "Europe/Moscow" +author: "Головин Г.Г." +author_translated: "Golovin G.G." +translation_caption: "translation from Russian" +# build parameters +disable_disk_cache: true +theme: older-tomato-theme +defaults: + - scope: + path: "" + values: + layout: default diff --git a/jekyll_site/_includes/counters_body.html b/jekyll_site/_includes/counters_body.html new file mode 100644 index 0000000..269f6ab --- /dev/null +++ b/jekyll_site/_includes/counters_body.html @@ -0,0 +1,2 @@ + + diff --git a/jekyll_site/_includes/counters_head.html b/jekyll_site/_includes/counters_head.html new file mode 100644 index 0000000..637ad8a --- /dev/null +++ b/jekyll_site/_includes/counters_head.html @@ -0,0 +1,16 @@ + + + + + diff --git a/jekyll_site/en/2021/12/10/optimizing-matrix-multiplication.md b/jekyll_site/en/2021/12/10/optimizing-matrix-multiplication.md new file mode 100644 index 0000000..f431e77 --- /dev/null +++ b/jekyll_site/en/2021/12/10/optimizing-matrix-multiplication.md @@ -0,0 +1,248 @@ +--- +title: Optimizing matrix multiplication +description: Consider an algorithm for multiplying matrices using three nested loops. The complexity of such an algorithm by definition should be O(n³), but there are... +sections: [Permutations,Nested loops,Comparing algorithms] +tags: [java,arrays,multidimensional arrays,matrices,rows,columns,layers,loops] +canonical_url: /en/2021/12/10/optimizing-matrix-multiplication.html +url_translated: /ru/2021/12/09/optimizing-matrix-multiplication.html +title_translated: Оптимизация умножения матриц +date: 2021.12.10 +lang: en +--- + +Consider an algorithm for multiplying matrices using three nested loops. The complexity of such +an algorithm by definition should be `O(n³)`, but there are particularities related to the execution +environment — the speed of the algorithm depends on the sequence in which the loops are executed. + +Let's compare different permutations of nested loops and the execution time of the algorithms. +Let's take two matrices: {`L×M`} and {`M×N`} → three loops → six permutations: +`LMN`, `LNM`, `MLN`, `MNL`, `NLM`, `NML`. + +The algorithms that work faster than others are those that write data to the resulting matrix +*row-wise in layers*: `LMN` and `MLN`, — the percentage difference to other algorithms is substantial +and depends on the execution environment. + +*Further optimization: [Matrix multiplication in parallel streams]({{ '/en/2022/02/09/matrix-multiplication-parallel-streams.html' | relative_url }}).* + +## Row-wise algorithm {#row-wise-algorithm} + +The outer loop bypasses the rows of the first matrix `L`, then there is a loop across the *common side* +of the two matrices `M` and it is followed by a loop across the columns of the second matrix `N`. +Writing to the resulting matrix occurs row-wise, and each row is filled in layers. + +```java +/** + * @param l rows of matrix 'a' + * @param m columns of matrix 'a' + * and rows of matrix 'b' + * @param n columns of matrix 'b' + * @param a first matrix 'l×m' + * @param b second matrix 'm×n' + * @return resulting matrix 'l×n' + */ +public static int[][] matrixMultiplicationLMN(int l, int m, int n, int[][] a, int[][] b) { + // resulting matrix + int[][] c = new int[l][n]; + // bypass the indexes of the rows of matrix 'a' + for (int i = 0; i < l; i++) + // bypass the indexes of the common side of two matrices: + // the columns of matrix 'a' and the rows of matrix 'b' + for (int k = 0; k < m; k++) + // bypass the indexes of the columns of matrix 'b' + for (int j = 0; j < n; j++) + // the sum of the products of the elements of the i-th + // row of matrix 'a' and the j-th column of matrix 'b' + c[i][j] += a[i][k] * b[k][j]; + return c; +} +``` + +## Layer-wise algorithm {#layer-wise-algorithm} + +The outer loop bypasses the *common side* of the two matrices `M`, then there is a loop across the rows +of the first matrix `L`, and it is followed by a loop across the columns of the second matrix `N`. +Writing to the resulting matrix occurs layer-wise, and each layer is filled row-wise. + +```java +/** + * @param l rows of matrix 'a' + * @param m columns of matrix 'a' + * and rows of matrix 'b' + * @param n columns of matrix 'b' + * @param a first matrix 'l×m' + * @param b second matrix 'm×n' + * @return resulting matrix 'l×n' + */ +public static int[][] matrixMultiplicationMLN(int l, int m, int n, int[][] a, int[][] b) { + // resulting matrix + int[][] c = new int[l][n]; + // bypass the indexes of the common side of two matrices: + // the columns of matrix 'a' and the rows of matrix 'b' + for (int k = 0; k < m; k++) + // bypass the indexes of the rows of matrix 'a' + for (int i = 0; i < l; i++) + // bypass the indexes of the columns of matrix 'b' + for (int j = 0; j < n; j++) + // the sum of the products of the elements of the i-th + // row of matrix 'a' and the j-th column of matrix 'b' + c[i][j] += a[i][k] * b[k][j]; + return c; +} +``` + +### Other algorithms {#other-algorithms} + +The bypass of the columns of the second matrix `N` occurs before the bypass of the *common side* of the +two matrices `M` and/or before the bypass of the rows of the first matrix `L`. + +{% capture collapsed_md %} +```java +public static int[][] matrixMultiplicationLNM(int l, int m, int n, int[][] a, int[][] b) { + int[][] c = new int[l][n]; + for (int i = 0; i < l; i++) + for (int j = 0; j < n; j++) + for (int k = 0; k < m; k++) + c[i][j] += a[i][k] * b[k][j]; + return c; +} +``` +```java +public static int[][] matrixMultiplicationNLM(int l, int m, int n, int[][] a, int[][] b) { + int[][] c = new int[l][n]; + for (int j = 0; j < n; j++) + for (int i = 0; i < l; i++) + for (int k = 0; k < m; k++) + c[i][j] += a[i][k] * b[k][j]; + return c; +} +``` +```java +public static int[][] matrixMultiplicationMNL(int l, int m, int n, int[][] a, int[][] b) { + int[][] c = new int[l][n]; + for (int k = 0; k < m; k++) + for (int j = 0; j < n; j++) + for (int i = 0; i < l; i++) + c[i][j] += a[i][k] * b[k][j]; + return c; +} +``` +```java +public static int[][] matrixMultiplicationNML(int l, int m, int n, int[][] a, int[][] b) { + int[][] c = new int[l][n]; + for (int j = 0; j < n; j++) + for (int k = 0; k < m; k++) + for (int i = 0; i < l; i++) + c[i][j] += a[i][k] * b[k][j]; + return c; +} +``` +{% endcapture %} +{%- include collapsed_block.html summary="Code without comments" content=collapsed_md -%} + +## Comparing algorithms {#comparing-algorithms} + +To check, we take two matrices `A=[500×700]` and `B=[700×450]`, filled with random numbers. First, we +compare the correctness of the implementation of the algorithms — all results obtained must match. +Then we execute each method 10 times and calculate the average execution time. + +```java +// start the program and output the result +public static void main(String[] args) throws Exception { + // incoming data + int l = 500, m = 700, n = 450, steps = 10; + int[][] a = randomMatrix(l, m), b = randomMatrix(m, n); + // map of methods for comparison + var methods = new TreeMap>(Map.of( + "LMN", () -> matrixMultiplicationLMN(l, m, n, a, b), + "LNM", () -> matrixMultiplicationLNM(l, m, n, a, b), + "MLN", () -> matrixMultiplicationMLN(l, m, n, a, b), + "MNL", () -> matrixMultiplicationMNL(l, m, n, a, b), + "NLM", () -> matrixMultiplicationNLM(l, m, n, a, b), + "NML", () -> matrixMultiplicationNML(l, m, n, a, b))); + int[][] last = null; + // bypass the methods map, check the correctness of the returned + // results, all results obtained must be equal to each other + for (var method : methods.entrySet()) { + // next method for comparison + var next = methods.higherEntry(method.getKey()); + // if the current method is not the last — compare the results of two methods + if (next != null) System.out.println(method.getKey() + "=" + next.getKey() + ": " + // compare the result of executing the current method and the next one + + Arrays.deepEquals(method.getValue().call(), next.getValue().call())); + // the result of the last method + else last = method.getValue().call(); + } + int[][] test = last; + // bypass the methods map, measure the execution time of each method + for (var method : methods.entrySet()) + // parameters: title, number of steps, runnable code + benchmark(method.getKey(), steps, () -> { + try { // execute the method, get the result + int[][] result = method.getValue().call(); + // check the correctness of the results at each step + if (!Arrays.deepEquals(result, test)) System.out.print("error"); + } catch (Exception e) { + e.printStackTrace(); + } + }); +} +``` + +{% capture collapsed_md %} +```java +// helper method, returns a matrix of the specified size +private static int[][] randomMatrix(int row, int col) { + int[][] matrix = new int[row][col]; + for (int i = 0; i < row; i++) + for (int j = 0; j < col; j++) + matrix[i][j] = (int) (Math.random() * row * col); + return matrix; +} +``` +```java +// helper method for measuring the execution time of the passed code +private static void benchmark(String title, int steps, Runnable runnable) { + long time, avg = 0; + System.out.print(title); + for (int i = 0; i < steps; i++) { + time = System.currentTimeMillis(); + runnable.run(); + time = System.currentTimeMillis() - time; + // execution time of one step + System.out.print(" | " + time); + avg += time; + } + // average execution time + System.out.println(" || " + (avg / steps)); +} +``` +{% endcapture %} +{%- include collapsed_block.html summary="Helper methods" content=collapsed_md -%} + +Output depends on the execution environment, time in milliseconds: + +``` +LMN=LNM: true +LNM=MLN: true +MLN=MNL: true +MNL=NLM: true +NLM=NML: true +LMN | 191 | 109 | 105 | 106 | 105 | 106 | 106 | 105 | 123 | 109 || 116 +LNM | 417 | 418 | 419 | 416 | 416 | 417 | 418 | 417 | 416 | 417 || 417 +MLN | 113 | 115 | 113 | 115 | 114 | 114 | 114 | 115 | 114 | 113 || 114 +MNL | 857 | 864 | 857 | 859 | 860 | 863 | 862 | 860 | 858 | 860 || 860 +NLM | 404 | 404 | 407 | 404 | 406 | 405 | 405 | 404 | 403 | 404 || 404 +NML | 866 | 872 | 867 | 868 | 867 | 868 | 867 | 873 | 869 | 863 || 868 +``` + +All the methods described above, including collapsed blocks, can be placed in one class. + +{% capture collapsed_md %} +```java +import java.util.Arrays; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.Callable; +``` +{% endcapture %} +{%- include collapsed_block.html summary="Required imports" content=collapsed_md -%} diff --git a/jekyll_site/en/2021/12/13/matrix-rotation-90-degrees.md b/jekyll_site/en/2021/12/13/matrix-rotation-90-degrees.md new file mode 100644 index 0000000..5fbc333 --- /dev/null +++ b/jekyll_site/en/2021/12/13/matrix-rotation-90-degrees.md @@ -0,0 +1,97 @@ +--- +title: Matrix rotation 90 degrees +description: Consider the algorithm for rotating a matrix 90 degrees clockwise and anticlockwise. This algorithm is similar to matrix transpose, with the difference... +sections: [Transpose,Comparing algorithms] +tags: [java,arrays,multidimensional arrays,matrices,rows,columns,loops,nested loops] +canonical_url: /en/2021/12/13/matrix-rotation-90-degrees.html +url_translated: /ru/2021/12/12/matrix-rotation-90-degrees.html +title_translated: Поворот матрицы на 90 градусов +date: 2021.12.13 +lang: en +--- + +Consider the algorithm for rotating a matrix 90 degrees clockwise and anticlockwise. This +algorithm is similar to *matrix transpose*, with the difference that here, for each point, +one of the coordinates is mirrored. + +```java +// matrix transpose — rows and columns are swapped +swapped[j][i] = matrix[i][j]; +// clockwise ↻ — rows are mirrored +rotated[j][i] = matrix[m-i-1][j]; +// anticlockwise ↺ — columns are mirrored +rotated[j][i] = matrix[i][n-j-1]; +``` + +*Similar algorithm: [Matrix rotation 180 degrees]({{ '/en/2021/12/17/matrix-rotation-180-degrees.html' | relative_url }}).* + +Let's write a method in Java to rotate a matrix {`m×n`} 90 degrees. The additional parameter +sets the direction of rotation: clockwise or anticlockwise. As an example, let's take a +rectangular matrix {`4×3`}. + +```java +/** + * @param m number of rows of the original matrix + * @param n number of columns of the original matrix + * @param clock direction of rotation: + * clockwise ↻ or anticlockwise ↺ + * @param matrix the original matrix + * @return the rotated matrix + */ +public static int[][] rotateMatrix(int m, int n, boolean clock, int[][] matrix) { + // new matrix, the number of rows and columns are swapped + int[][] rotated = new int[n][m]; + // bypass the rows of the original matrix + for (int i = 0; i < m; i++) + // bypass the columns of the original matrix + for (int j = 0; j < n; j++) + if (clock) // clockwise rotation ↻ + rotated[j][i] = matrix[m-i-1][j]; + else // anticlockwise rotation ↺ + rotated[j][i] = matrix[i][n-j-1]; + return rotated; +} +``` +{% raw %} +```java +// start the program and output the result +public static void main(String[] args) { + // incoming data + int m = 4, n = 3; + int[][] matrix = {{11, 12, 13}, {14, 15, 16}, {17, 18, 19}, {20, 21, 22}}; + // rotate the matrix and output the result + outputMatrix("Original matrix:", matrix); + outputMatrix("Clockwise ↻:", rotateMatrix(m, n, true, matrix)); + outputMatrix("Anticlockwise ↺:", rotateMatrix(m, n, false, matrix)); +} +``` +{% endraw %} +```java +// helper method, prints the matrix to the console row-wise +public static void outputMatrix(String title, int[][] matrix) { + System.out.println(title); + for (int[] row : matrix) { + for (int el : row) + System.out.print(" " + el); + System.out.println(); + } +} +``` + +Output: + +``` +Original matrix: + 11 12 13 + 14 15 16 + 17 18 19 + 20 21 22 +Clockwise ↻: + 20 17 14 11 + 21 18 15 12 + 22 19 16 13 +Anticlockwise ↺: + 13 16 19 22 + 12 15 18 21 + 11 14 17 20 +``` diff --git a/jekyll_site/en/2021/12/17/matrix-rotation-180-degrees.md b/jekyll_site/en/2021/12/17/matrix-rotation-180-degrees.md new file mode 100644 index 0000000..ecf91c9 --- /dev/null +++ b/jekyll_site/en/2021/12/17/matrix-rotation-180-degrees.md @@ -0,0 +1,85 @@ +--- +title: Matrix rotation 180 degrees +description: Consider the algorithm for rotating a matrix 180 degrees. Unlike the transpose algorithm, here in the resulting matrix the rows and columns are not swapped... +sections: [Transpose,Comparing algorithms] +tags: [java,arrays,multidimensional arrays,matrices,rows,columns,loops,nested loops] +canonical_url: /en/2021/12/17/matrix-rotation-180-degrees.html +url_translated: /ru/2021/12/16/matrix-rotation-180-degrees.html +title_translated: Поворот матрицы на 180 градусов +date: 2021.12.17 +lang: en +--- + +Consider the algorithm for rotating a matrix 180 degrees. Unlike the *transpose* algorithm, +here in the resulting matrix the rows and columns are not swapped, but are mirrored. + +```java +// rows and columns are swapped +swapped[j][i] = matrix[i][j]; +// rows and columns are mirrored +rotated[i][j] = matrix[m-i-1][n-j-1]; +``` + +*Similar algorithm: [Matrix rotation 90 degrees]({{ '/en/2021/12/13/matrix-rotation-90-degrees.html' | relative_url }}).* + +Let's write a method in Java to rotate a matrix {`m×n`} 180 degrees. As an example, +let's take a rectangular matrix {`4×3`}. + +```java +/** + * @param m number of rows of the original matrix + * @param n number of columns of the original matrix + * @param matrix the original matrix + * @return the rotated matrix + */ +public static int[][] rotateMatrix(int m, int n, int[][] matrix) { + // new matrix + int[][] rotated = new int[m][n]; + // bypass the rows of the original matrix + for (int i = 0; i < m; i++) + // bypass the columns of the original matrix + for (int j = 0; j < n; j++) + // rows and columns are mirrored + rotated[i][j] = matrix[m-i-1][n-j-1]; + return rotated; +} +``` +{% raw %} +```java +// start the program and output the result +public static void main(String[] args) { + // incoming data + int m = 4, n = 3; + int[][] matrix = {{11, 12, 13}, {14, 15, 16}, {17, 18, 19}, {20, 21, 22}}; + // rotate the matrix and output the result + outputMatrix("Original matrix:", matrix); + outputMatrix("Rotation by 180°:", rotateMatrix(m, n, matrix)); +} +``` +{% endraw %} +```java +// helper method, prints the matrix to the console row-wise +public static void outputMatrix(String title, int[][] matrix) { + System.out.println(title); + for (int[] row : matrix) { + for (int el : row) + System.out.print(" " + el); + System.out.println(); + } +} +``` + +Output: + +``` +Original matrix: + 11 12 13 + 14 15 16 + 17 18 19 + 20 21 22 +Rotation by 180°: + 22 21 20 + 19 18 17 + 16 15 14 + 13 12 11 +``` diff --git a/jekyll_site/en/2022/02/09/matrix-multiplication-parallel-streams.md b/jekyll_site/en/2022/02/09/matrix-multiplication-parallel-streams.md new file mode 100644 index 0000000..1ae836a --- /dev/null +++ b/jekyll_site/en/2022/02/09/matrix-multiplication-parallel-streams.md @@ -0,0 +1,184 @@ +--- +title: Matrix multiplication in parallel streams +description: Consider an algorithm for multiplying rectangular matrices using Java Streams. Let's take the optimized version of the algorithm using three nested loops... +sections: [Multithreading,Nested loops,Comparing algorithms] +tags: [java,streams,arrays,multidimensional arrays,matrices,rows,loops] +canonical_url: /en/2022/02/09/matrix-multiplication-parallel-streams.html +url_translated: /ru/2022/02/08/matrix-multiplication-parallel-streams.html +title_translated: Умножение матриц в параллельных потоках +date: 2022.02.09 +lang: en +--- + +Consider an algorithm for multiplying rectangular matrices using Java Streams. Let's take the +*optimized version* of the algorithm using three nested loops and replace the outer loop with +a stream. Let's compare the operating time of two algorithms. + +We process the rows of the resulting matrix in parallel mode, and populate each row layerwise. Due to +the parallel use of cache of the execution environment on multi-core machines, the computation time can +be reduced by more than two times. To check, let's take two rectangular matrices {`L×M`} and {`M×N`}. + +*Further optimization: [Winograd — Strassen algorithm]({{ '/en/2022/02/11/winograd-strassen-algorithm.html' | relative_url }}).* + +## Parallel stream {#parallel-stream} + +We bypass the rows of the first matrix `L` in parallel mode. In each thread there are two nested loops: +across the *common side* of two matrices `M` and across the columns of the second matrix `N`. Processing +of the rows of the resulting matrix occurs independently of each other in parallel streams. + +```java +/** + * @param l rows of matrix 'a' + * @param m columns of matrix 'a' + * and rows of matrix 'b' + * @param n columns of matrix 'b' + * @param a first matrix 'l×m' + * @param b second matrix 'm×n' + * @return resulting matrix 'l×n' + */ +public static int[][] parallelMatrixMultiplication(int l, int m, int n, int[][] a, int[][] b) { + // resulting matrix + int[][] c = new int[l][n]; + // bypass the indexes of the rows of matrix 'a' in parallel mode + IntStream.range(0, l).parallel().forEach(i -> { + // bypass the indexes of the common side of two matrices: + // the columns of matrix 'a' and the rows of matrix 'b' + for (int k = 0; k < m; k++) + // bypass the indexes of the columns of matrix 'b' + for (int j = 0; j < n; j++) + // the sum of the products of the elements of the i-th + // row of matrix 'a' and the j-th column of matrix 'b' + c[i][j] += a[i][k] * b[k][j]; + }); + return c; +} +``` + +## Three nested loops {#three-nested-loops} + +We take the *optimized* variant of algorithm, that uses cache of the execution environment better +than others. The outer loop bypasses the rows of the first matrix `L`, then there is a loop across +the *common side* of the two matrices `M` and it is followed by a loop across the columns of the +second matrix `N`. Writing to the resulting matrix occurs row-wise, and each row is filled in layers. + +```java +/** + * @param l rows of matrix 'a' + * @param m columns of matrix 'a' + * and rows of matrix 'b' + * @param n columns of matrix 'b' + * @param a first matrix 'l×m' + * @param b second matrix 'm×n' + * @return resulting matrix 'l×n' + */ +public static int[][] sequentialMatrixMultiplication(int l, int m, int n, int[][] a, int[][] b) { + // resulting matrix + int[][] c = new int[l][n]; + // bypass the indexes of the rows of matrix 'a' + for (int i = 0; i < l; i++) + // bypass the indexes of the common side of two matrices: + // the columns of matrix 'a' and the rows of matrix 'b' + for (int k = 0; k < m; k++) + // bypass the indexes of the columns of matrix 'b' + for (int j = 0; j < n; j++) + // the sum of the products of the elements of the i-th + // row of matrix 'a' and the j-th column of matrix 'b' + c[i][j] += a[i][k] * b[k][j]; + return c; +} +``` + +## Testing {#testing} + +To check, we take two matrices `A=[900×1000]` and `B=[1000×750]`, filled with random numbers. +First, we compare the correctness of the implementation of the two algorithms — matrix products +must match. Then we execute each method 10 times and calculate the average execution time. + +```java +// start the program and output the result +public static void main(String[] args) { + // incoming data + int l = 900, m = 1000, n = 750, steps = 10; + int[][] a = randomMatrix(l, m), b = randomMatrix(m, n); + // matrix products + int[][] c1 = parallelMatrixMultiplication(l, m, n, a, b); + int[][] c2 = sequentialMatrixMultiplication(l, m, n, a, b); + // check the correctness of the results + System.out.println("The results match: " + Arrays.deepEquals(c1, c2)); + // measure the execution time of two methods + benchmark("Stream", steps, () -> { + int[][] c = parallelMatrixMultiplication(l, m, n, a, b); + if (!Arrays.deepEquals(c, c1)) System.out.print("error"); + }); + benchmark("Loops", steps, () -> { + int[][] c = sequentialMatrixMultiplication(l, m, n, a, b); + if (!Arrays.deepEquals(c, c2)) System.out.print("error"); + }); +} +``` + +{% capture collapsed_md %} +```java +// helper method, returns a matrix of the specified size +private static int[][] randomMatrix(int row, int col) { + int[][] matrix = new int[row][col]; + for (int i = 0; i < row; i++) + for (int j = 0; j < col; j++) + matrix[i][j] = (int) (Math.random() * row * col); + return matrix; +} +``` +```java +// helper method for measuring the execution time of the passed code +private static void benchmark(String title, int steps, Runnable runnable) { + long time, avg = 0; + System.out.print(title); + for (int i = 0; i < steps; i++) { + time = System.currentTimeMillis(); + runnable.run(); + time = System.currentTimeMillis() - time; + // execution time of one step + System.out.print(" | " + time); + avg += time; + } + // average execution time + System.out.println(" || " + (avg / steps)); +} +``` +{% endcapture %} +{%- include collapsed_block.html summary="Helper methods" content=collapsed_md -%} + +Output depends on the execution environment, time in milliseconds: + +``` +The results match: true +Stream | 113 | 144 | 117 | 114 | 114 | 117 | 120 | 125 | 111 | 113 || 118 +Loops | 1357 | 530 | 551 | 569 | 535 | 538 | 525 | 517 | 518 | 514 || 615 +``` + +## Comparing algorithms {#comparing-algorithms} + +On an eight-core Linux x64 computer, we create a Windows x64 virtual machine for tests. All +other things being equal, in the settings, we change the number of processors. We run the +above test 100 times instead of 10 — we get a summary table of results. Time in milliseconds. + +``` + CPU | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | +-------|-----|-----|-----|-----|-----|-----|-----|-----| +Stream | 522 | 276 | 179 | 154 | 128 | 112 | 110 | 93 | +Loops | 519 | 603 | 583 | 571 | 545 | 571 | 559 | 467 | +``` + +Results: with a large number of processors in the system, the multithreaded algorithm works +out noticeably faster than three nested loops. The computation time can be reduced by more +than two times. + +All the methods described above, including collapsed blocks, can be placed in one class. + +{% capture collapsed_md %} +```java +import java.util.Arrays; +import java.util.stream.IntStream; +``` +{% endcapture %} +{%- include collapsed_block.html summary="Required imports" content=collapsed_md -%} diff --git a/jekyll_site/en/2022/02/11/winograd-strassen-algorithm.md b/jekyll_site/en/2022/02/11/winograd-strassen-algorithm.md new file mode 100644 index 0000000..5a76fc1 --- /dev/null +++ b/jekyll_site/en/2022/02/11/winograd-strassen-algorithm.md @@ -0,0 +1,298 @@ +--- +title: Winograd — Strassen algorithm +description: Consider a modification of Strassen's algorithm for square matrix multiplication with fewer number of summations between blocks than in the ordinary... +sections: [Multithreading,Block matrices,Comparing algorithms] +tags: [java,streams,arrays,multidimensional arrays,matrices,recursion,loops,nested loops] +canonical_url: /en/2022/02/11/winograd-strassen-algorithm.html +url_translated: /ru/2022/02/10/winograd-strassen-algorithm.html +title_translated: Алгоритм Винограда — Штрассена +date: 2022.02.11 +lang: en +--- + +Consider a modification of Strassen's algorithm for square matrix multiplication with *fewer* number +of summations between blocks than in the ordinary algorithm — 15 instead of 18 and the same number +of multiplications as in the ordinary algorithm — 7. We will use Java Streams. + +Recursive partitioning of matrices into blocks during multiplication makes sense up to a certain +limit, and then it loses its sense, since the Strassen's algorithm does not use cache of the execution +environment. Therefore, for small blocks we will use a parallel version of nested loops, and for large +blocks we will perform recursive partitioning in parallel. + +We determine the boundary between the two algorithms experimentally — we adjust it to the cache of the +execution environment. The benefit of Strassen's algorithm becomes more evident on sizable matrices — +the difference with the algorithm using nested loops becomes larger and depends on the execution +environment. Let's compare the operating time of two algorithms. + +*Algorithm using three nested loops: [Optimizing matrix multiplication]({{ '/en/2021/12/10/optimizing-matrix-multiplication.html' | relative_url }}).* + +## Algorithm description {#algorithm-description} + +Matrices must be the same size. We partition each matrix into 4 equally sized blocks. The blocks +must be square, therefore if this is not the case, then first we supplement the matrices with zero +rows and columns, and after that partition them into blocks. We will remove the redundant rows and +columns later from the resulting matrix. + +{% include image_svg.html src="/img/block-matrices.svg" style="width:221pt; height:33pt;" +alt="{\displaystyle A={\begin{pmatrix}A_{11}&A_{12}\\A_{21}&A_{22}\end{pmatrix}},\quad B={\begin{pmatrix}B_{11}&B_{12}\\B_{21}&B_{22}\end{pmatrix}}.}" %} + +Summation of blocks. + +{% include image_svg.html src="/img/sums1.svg" style="width:101pt; height:148pt;" +alt="{\displaystyle{\begin{aligned}S_{1}&=(A_{21}+A_{22});\\S_{2}&=(S_{1}-A_{11});\\S_{3}&=(A_{11}-A_{21});\\S_{4}&=(A_{12}-S_{2});\\S_{5}&=(B_{12}-B_{11});\\S_{6}&=(B_{22}-S_{5});\\S_{7}&=(B_{22}-B_{12});\\S_{8}&=(S_{6}-B_{21}).\end{aligned}}}" %} + +Multiplication of blocks. + +{% include image_svg.html src="/img/products.svg" style="width:75pt; height:127pt;" +alt="{\displaystyle{\begin{aligned}P_{1}&=S_{2}S_{6};\\P_{2}&=A_{11}B_{11};\\P_{3}&=A_{12}B_{21};\\P_{4}&=S_{3}S_{7};\\P_{5}&=S_{1}S_{5};\\P_{6}&=S_{4}B_{22};\\P_{7}&=A_{22}S_{8}.\end{aligned}}}" %} + +Summation of blocks. + +{% include image_svg.html src="/img/sums2.svg" style="width:78pt; height:31pt;" +alt="{\displaystyle{\begin{aligned}T_{1}&=P_{1}+P_{2};\\T_{2}&=T_{1}+P_{4}.\end{aligned}}}" %} + +Blocks of the resulting matrix. + +{% include image_svg.html src="/img/sums3.svg" style="width:240pt; height:33pt;" +alt="{\displaystyle{\begin{pmatrix}C_{11}&C_{12}\\C_{21}&C_{22}\end{pmatrix}}={\begin{pmatrix}P_{2}+P_{3}&T_{1}+P_{5}+P_{6}\\T_{2}-P_{7}&T_{2}+P_{5}\end{pmatrix}}.}" %} + +## Hybrid algorithm {#hybrid-algorithm} + +We partition each matrix `A` and `B` into 4 equally sized blocks and, if necessary, we supplement +the missing parts with zeros. Perform 15 summations and 7 multiplications over the blocks — we get +4 blocks of the matrix `C`. Remove the redundant zeros, if added, and return the resulting matrix. +We run recursive partitioning of large blocks in parallel mode, and for small blocks we call the +algorithm with nested loops. + +```java +/** + * @param n matrix size + * @param brd minimum matrix size + * @param a first matrix 'n×n' + * @param b second matrix 'n×n' + * @return resulting matrix 'n×n' + */ +public static int[][] multiplyMatrices(int n, int brd, int[][] a, int[][] b) { + // multiply small blocks using algorithm with nested loops + if (n < brd) return simpleMultiplication(n, a, b); + // midpoint of the matrix, round up — blocks should + // be square, if necessary add zero rows and columns + int m = n - n / 2; + // blocks of the first matrix + int[][] a11 = getQuadrant(m, n, a, true, true); + int[][] a12 = getQuadrant(m, n, a, true, false); + int[][] a21 = getQuadrant(m, n, a, false, true); + int[][] a22 = getQuadrant(m, n, a, false, false); + // blocks of the second matrix + int[][] b11 = getQuadrant(m, n, b, true, true); + int[][] b12 = getQuadrant(m, n, b, true, false); + int[][] b21 = getQuadrant(m, n, b, false, true); + int[][] b22 = getQuadrant(m, n, b, false, false); + // summation of blocks + int[][] s1 = sumMatrices(m, a21, a22, true); + int[][] s2 = sumMatrices(m, s1, a11, false); + int[][] s3 = sumMatrices(m, a11, a21, false); + int[][] s4 = sumMatrices(m, a12, s2, false); + int[][] s5 = sumMatrices(m, b12, b11, false); + int[][] s6 = sumMatrices(m, b22, s5, false); + int[][] s7 = sumMatrices(m, b22, b12, false); + int[][] s8 = sumMatrices(m, s6, b21, false); + int[][][] p = new int[7][][]; + // multiplication of blocks in parallel streams + IntStream.range(0, 7).parallel().forEach(i -> { + switch (i) { // recursive calls + case 0: p[i] = multiplyMatrices(m, brd, s2, s6); break; + case 1: p[i] = multiplyMatrices(m, brd, a11, b11); break; + case 2: p[i] = multiplyMatrices(m, brd, a12, b21); break; + case 3: p[i] = multiplyMatrices(m, brd, s3, s7); break; + case 4: p[i] = multiplyMatrices(m, brd, s1, s5); break; + case 5: p[i] = multiplyMatrices(m, brd, s4, b22); break; + case 6: p[i] = multiplyMatrices(m, brd, a22, s8); break; + } + }); + // summation of blocks + int[][] t1 = sumMatrices(m, p[0], p[1], true); + int[][] t2 = sumMatrices(m, t1, p[3], true); + // blocks of the resulting matrix + int[][] c11 = sumMatrices(m, p[1], p[2], true); + int[][] c12 = sumMatrices(m, t1, sumMatrices(m, p[4], p[5], true), true); + int[][] c21 = sumMatrices(m, t2, p[6], false); + int[][] c22 = sumMatrices(m, t2, p[4], true); + // assemble a matrix from blocks, + // remove zero rows and columns, if added + return putQuadrants(m, n, c11, c12, c21, c22); +} +``` + +{% capture collapsed_md %} +```java +// helper method for matrix summation +private static int[][] sumMatrices(int n, int[][] a, int[][] b, boolean sign) { + int[][] c = new int[n][n]; + for (int i = 0; i < n; i++) + for (int j = 0; j < n; j++) + c[i][j] = sign ? a[i][j] + b[i][j] : a[i][j] - b[i][j]; + return c; +} +``` +```java +// helper method, gets a block of a matrix +private static int[][] getQuadrant(int m, int n, int[][] x, + boolean first, boolean second) { + int[][] q = new int[m][m]; + if (first) for (int i = 0; i < m; i++) + if (second) System.arraycopy(x[i], 0, q[i], 0, m); // x11 + else System.arraycopy(x[i], m, q[i], 0, n - m); // x12 + else for (int i = m; i < n; i++) + if (second) System.arraycopy(x[i], 0, q[i - m], 0, m); // x21 + else System.arraycopy(x[i], m, q[i - m], 0, n - m); // x22 + return q; +} +``` +```java +// helper method, assembles a matrix from blocks +private static int[][] putQuadrants(int m, int n, + int[][] x11, int[][] x12, + int[][] x21, int[][] x22) { + int[][] x = new int[n][n]; + for (int i = 0; i < n; i++) + if (i < m) { + System.arraycopy(x11[i], 0, x[i], 0, m); + System.arraycopy(x12[i], 0, x[i], m, n - m); + } else { + System.arraycopy(x21[i - m], 0, x[i], 0, m); + System.arraycopy(x22[i - m], 0, x[i], m, n - m); + } + return x; +} +``` +{% endcapture %} +{%- include collapsed_block.html summary="Helper methods" content=collapsed_md -%} + +## Nested loops {#nested-loops} + +To supplement the previous algorithm and to compare with it, we take the *optimized* variant of nested +loops, that uses cache of the execution environment better than others — processing of the rows of the +resulting matrix occurs independently of each other in parallel streams. For small matrices, we use this +algorithm — large matrices we partition into small blocks and use the same algorithm. + +```java +/** + * @param n matrix size + * @param a first matrix 'n×n' + * @param b second matrix 'n×n' + * @return resulting matrix 'n×n' + */ +public static int[][] simpleMultiplication(int n, int[][] a, int[][] b) { + // the resulting matrix + int[][] c = new int[n][n]; + // bypass the rows of matrix 'a' in parallel mode + IntStream.range(0, n).parallel().forEach(i -> { + // bypass the indexes of the common side of two matrices: + // the columns of matrix 'a' and the rows of matrix 'b' + for (int k = 0; k < n; k++) + // bypass the indexes of the columns of matrix 'b' + for (int j = 0; j < n; j++) + // the sum of the products of the elements of the i-th + // row of matrix 'a' and the j-th column of matrix 'b' + c[i][j] += a[i][k] * b[k][j]; + }); + return c; +} +``` + +## Testing {#testing} + +To check, we take two square matrices `A=[1000×1000]` and `B=[1000×1000]`, filled with random numbers. +Take the minimum block size `[200×200]` elements. First, we compare the correctness of the implementation +of the two algorithms — matrix products must match. Then we execute each method 10 times and calculate +the average execution time. + +```java +// start the program and output the result +public static void main(String[] args) { + // incoming data + int n = 1000, brd = 200, steps = 10; + int[][] a = randomMatrix(n, n), b = randomMatrix(n, n); + // matrix products + int[][] c1 = multiplyMatrices(n, brd, a, b); + int[][] c2 = simpleMultiplication(n, a, b); + // check the correctness of the results + System.out.println("The results match: " + Arrays.deepEquals(c1, c2)); + // measure the execution time of two methods + benchmark("Hybrid algorithm", steps, () -> { + int[][] c = multiplyMatrices(n, brd, a, b); + if (!Arrays.deepEquals(c, c1)) System.out.print("error"); + }); + benchmark("Nested loops ", steps, () -> { + int[][] c = simpleMultiplication(n, a, b); + if (!Arrays.deepEquals(c, c2)) System.out.print("error"); + }); +} +``` + +{% capture collapsed_md %} +```java +// helper method, returns a matrix of the specified size +private static int[][] randomMatrix(int row, int col) { + int[][] matrix = new int[row][col]; + for (int i = 0; i < row; i++) + for (int j = 0; j < col; j++) + matrix[i][j] = (int) (Math.random() * row * col); + return matrix; +} +``` +```java +// helper method for measuring the execution time of the passed code +private static void benchmark(String title, int steps, Runnable runnable) { + long time, avg = 0; + System.out.print(title); + for (int i = 0; i < steps; i++) { + time = System.currentTimeMillis(); + runnable.run(); + time = System.currentTimeMillis() - time; + // execution time of one step + System.out.print(" | " + time); + avg += time; + } + // average execution time + System.out.println(" || " + (avg / steps)); +} +``` +{% endcapture %} +{%- include collapsed_block.html summary="Helper methods" content=collapsed_md -%} + +Output depends on the execution environment, time in milliseconds: + +``` +The results match: true +Hybrid algorithm | 196 | 177 | 156 | 205 | 154 | 165 | 133 | 118 | 132 | 134 || 157 +Nested loops | 165 | 164 | 168 | 167 | 168 | 168 | 170 | 179 | 173 | 168 || 169 +``` + +## Comparing algorithms {#comparing-algorithms} + +On an eight-core Linux x64 computer, execute the above test 100 times instead of 10. Take the minimum +block size `[brd=200]` elements. Change only `n` — sizes of both matrices `A=[n×n]` and `B=[n×n]`. Get +a summary table of results. Time in milliseconds. + +``` + n | 900 | 1000 | 1100 | 1200 | 1300 | 1400 | 1500 | 1600 | 1700 | +-----------------|-----|------|------|------|------|------|------|------|------| +Hybrid algorithm | 96 | 125 | 169 | 204 | 260 | 313 | 384 | 482 | 581 | +Nested loops | 119 | 162 | 235 | 281 | 361 | 497 | 651 | 793 | 971 | +``` + +Results: the benefit of the Strassen algorithm becomes more evident on large matrices, when the +size of the matrix itself is several times larger than the size of the minimal block, and depends +on the execution environment. + +All the methods described above, including collapsed blocks, can be placed in one class. + +{% capture collapsed_md %} +```java +import java.util.Arrays; +import java.util.stream.IntStream; +``` +{% endcapture %} +{%- include collapsed_block.html summary="Required imports" content=collapsed_md -%} diff --git a/jekyll_site/en/index.md b/jekyll_site/en/index.md new file mode 100644 index 0000000..93f6b72 --- /dev/null +++ b/jekyll_site/en/index.md @@ -0,0 +1,76 @@ +--- +title: Code with comments +description: Notes about programming with code snippets and comments. Problem solutions and solution descriptions. +sections: [Problem solutions and solution descriptions] +tags: [java,algorithms,implementation,arrays,multidimensional arrays,matrices,loops,streams] +canonical_url: /en/ +url_translated: /ru/ +title_translated: Код с комментариями +lang: en +--- + +{%- assign articles = "" | split: "" %} +{%- assign articles = articles | push: "Winograd — Strassen algorithm" %} +{%- capture article_brief %} +Consider a modification of Strassen's algorithm for square matrix multiplication with *fewer* number +of summations between blocks than in the ordinary algorithm — 15 instead of 18 and the same number +of multiplications as in the ordinary algorithm — 7. We will use Java Streams. + +Recursive partitioning of matrices into blocks during multiplication makes sense up to a certain +limit, and then it loses its sense, since the Strassen's algorithm does not use cache of the execution +environment. Therefore, for small blocks we will use a parallel version of nested loops, and for large +blocks we will perform recursive partitioning in parallel. + +We determine the boundary between the two algorithms experimentally — we adjust it to the cache of the +execution environment. The benefit of Strassen's algorithm becomes more evident on sizable matrices — +the difference with the algorithm using nested loops becomes larger and depends on the execution +environment. Let's compare the operating time of two algorithms. +{%- endcapture %} +{%- assign articles = articles | push: article_brief %} +{%- assign articles = articles | push: "Matrix multiplication in parallel streams" %} +{%- capture article_brief %} +Consider an algorithm for multiplying rectangular matrices using Java Streams. Let's take the +*optimized version* of the algorithm using three nested loops and replace the outer loop with +a stream. Let's compare the operating time of two algorithms. + +We process the rows of the resulting matrix in parallel mode, and populate each row layerwise. Due to +the parallel use of cache of the execution environment on multi-core machines, the computation time can +be reduced by more than two times. To check, let's take two rectangular matrices {`L×M`} and {`M×N`}. +{%- endcapture %} +{%- assign articles = articles | push: article_brief %} +{%- assign articles = articles | push: "Matrix rotation 180 degrees" %} +{%- capture article_brief %} +Consider the algorithm for rotating a matrix 180 degrees. Unlike the *transpose* algorithm, +here in the resulting matrix the rows and columns are not swapped, but are mirrored. + +Let's write a method in Java to rotate a matrix {`m×n`} 180 degrees. As an example, +let's take a rectangular matrix {`4×3`}. +{%- endcapture %} +{%- assign articles = articles | push: article_brief %} +{%- assign articles = articles | push: "Matrix rotation 90 degrees" %} +{%- capture article_brief %} +Consider the algorithm for rotating a matrix 90 degrees clockwise and anticlockwise. This +algorithm is similar to *matrix transpose*, with the difference that here, for each point, +one of the coordinates is mirrored. + +Let's write a method in Java to rotate a matrix {`m×n`} 90 degrees. The additional parameter +sets the direction of rotation: clockwise or anticlockwise. As an example, let's take a +rectangular matrix {`4×3`}. +{%- endcapture %} +{%- assign articles = articles | push: article_brief %} +{%- assign articles = articles | push: "Optimizing matrix multiplication" %} +{%- capture article_brief %} +Consider an algorithm for multiplying matrices using three nested loops. The complexity of such +an algorithm by definition should be `O(n³)`, but there are particularities related to the execution +environment — the speed of the algorithm depends on the sequence in which the loops are executed. + +Let's compare different permutations of nested loops and the execution time of the algorithms. +Let's take two matrices: {`L×M`} and {`M×N`} → three loops → six permutations: +`LMN`, `LNM`, `MLN`, `MNL`, `NLM`, `NML`. + +The algorithms that work faster than others are those that write data to the resulting matrix +*row-wise in layers*: `LMN` and `MLN`, — the percentage difference to other algorithms is substantial +and depends on the execution environment. +{%- endcapture %} +{%- assign articles = articles | push: article_brief %} +{%- include main_page.html articles = articles -%} diff --git a/jekyll_site/img/block-matrices.svg b/jekyll_site/img/block-matrices.svg new file mode 100644 index 0000000..4922908 --- /dev/null +++ b/jekyll_site/img/block-matrices.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jekyll_site/img/products.svg b/jekyll_site/img/products.svg new file mode 100644 index 0000000..4d45eb4 --- /dev/null +++ b/jekyll_site/img/products.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jekyll_site/img/sums1.svg b/jekyll_site/img/sums1.svg new file mode 100644 index 0000000..1551ed8 --- /dev/null +++ b/jekyll_site/img/sums1.svg @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jekyll_site/img/sums2.svg b/jekyll_site/img/sums2.svg new file mode 100644 index 0000000..72abbac --- /dev/null +++ b/jekyll_site/img/sums2.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jekyll_site/img/sums3.svg b/jekyll_site/img/sums3.svg new file mode 100644 index 0000000..2ee22df --- /dev/null +++ b/jekyll_site/img/sums3.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jekyll_site/robots.txt b/jekyll_site/robots.txt new file mode 100644 index 0000000..49bee10 --- /dev/null +++ b/jekyll_site/robots.txt @@ -0,0 +1,7 @@ +User-agent: * +Disallow: *404* + +Sitemap: https://pomodoro3.mircloud.ru/pagesmap.xml +Sitemap: https://pomodoro3.mircloud.ru/color/pagesmap.xml + +Host: https://pomodoro3.mircloud.ru diff --git a/jekyll_site/ru/2021/12/09/optimizing-matrix-multiplication.md b/jekyll_site/ru/2021/12/09/optimizing-matrix-multiplication.md new file mode 100644 index 0000000..1b0d2a5 --- /dev/null +++ b/jekyll_site/ru/2021/12/09/optimizing-matrix-multiplication.md @@ -0,0 +1,246 @@ +--- +title: Оптимизация умножения матриц +description: Рассмотрим алгоритм перемножения матриц с использованием трёх вложенных циклов. Сложность такого алгоритма по определению должна составлять O(n³), но есть... +sections: [Перестановки,Вложенные циклы,Сравнение алгоритмов] +tags: [java,массивы,многомерные массивы,матрицы,строки,колонки,слои,циклы] +canonical_url: /ru/2021/12/09/optimizing-matrix-multiplication.html +url_translated: /en/2021/12/10/optimizing-matrix-multiplication.html +title_translated: Optimizing matrix multiplication +date: 2021.12.09 +--- + +Рассмотрим алгоритм перемножения матриц с использованием трёх вложенных циклов. Сложность такого +алгоритма по определению должна составлять `O(n³)`, но есть особенности, связанные со средой +выполнения — скорость работы алгоритма зависит от последовательности, в которой выполняются циклы. + +Сравним различные варианты перестановок вложенных циклов и время выполнения алгоритмов. +Возьмём две матрицы: {`L×M`} и {`M×N`} → три цикла → шесть перестановок: +`LMN`, `LNM`, `MLN`, `MNL`, `NLM`, `NML`. + +Быстрее других отрабатывают те алгоритмы, которые пишут данные в результирующую матрицу *построчно слоями*: +`LMN` и `MLN`, — разница в процентах к другим алгоритмам значительная и зависит от среды выполнения. + +*Дальнейшая оптимизация: [Умножение матриц в параллельных потоках]({{ '/ru/2022/02/08/matrix-multiplication-parallel-streams.html' | relative_url }}).* + +## Построчный алгоритм {#row-wise-algorithm} + +Внешний цикл обходит строки первой матрицы `L`, далее идёт цикл по *общей стороне* двух матриц `M` +и за ним цикл по колонкам второй матрицы `N`. Запись в результирующую матрицу происходит построчно, +а каждая строка заполняется слоями. + +```java +/** + * @param l строки матрицы 'a' + * @param m колонки матрицы 'a' + * и строки матрицы 'b' + * @param n колонки матрицы 'b' + * @param a первая матрица 'l×m' + * @param b вторая матрица 'm×n' + * @return результирующая матрица 'l×n' + */ +public static int[][] matrixMultiplicationLMN(int l, int m, int n, int[][] a, int[][] b) { + // результирующая матрица + int[][] c = new int[l][n]; + // обходим индексы строк матрицы 'a' + for (int i = 0; i < l; i++) + // обходим индексы общей стороны двух матриц: + // колонок матрицы 'a' и строк матрицы 'b' + for (int k = 0; k < m; k++) + // обходим индексы колонок матрицы 'b' + for (int j = 0; j < n; j++) + // сумма произведений элементов i-ой строки + // матрицы 'a' и j-ой колонки матрицы 'b' + c[i][j] += a[i][k] * b[k][j]; + return c; +} +``` + +## Послойный алгоритм {#layer-wise-algorithm} + +Внешний цикл обходит *общую сторону* двух матриц `M`, далее идёт цикл по строкам первой матрицы +`L` и за ним цикл по колонкам второй матрицы `N`. Запись в результирующую матрицу происходит слоями, +а каждый слой заполняется построчно. + +```java +/** + * @param l строки матрицы 'a' + * @param m колонки матрицы 'a' + * и строки матрицы 'b' + * @param n колонки матрицы 'b' + * @param a первая матрица 'l×m' + * @param b вторая матрица 'm×n' + * @return результирующая матрица 'l×n' + */ +public static int[][] matrixMultiplicationMLN(int l, int m, int n, int[][] a, int[][] b) { + // результирующая матрица + int[][] c = new int[l][n]; + // обходим индексы общей стороны двух матриц: + // колонок матрицы 'a' и строк матрицы 'b' + for (int k = 0; k < m; k++) + // обходим индексы строк матрицы 'a' + for (int i = 0; i < l; i++) + // обходим индексы колонок матрицы 'b' + for (int j = 0; j < n; j++) + // сумма произведений элементов i-ой строки + // матрицы 'a' и j-ой колонки матрицы 'b' + c[i][j] += a[i][k] * b[k][j]; + return c; +} +``` + +### Прочие алгоритмы {#other-algorithms} + +Обход колонок второй матрицы `N` происходит перед обходом *общей стороны* двух матриц `M` +и/или перед обходом строк первой матрицы `L`. + +{% capture collapsed_md %} +```java +public static int[][] matrixMultiplicationLNM(int l, int m, int n, int[][] a, int[][] b) { + int[][] c = new int[l][n]; + for (int i = 0; i < l; i++) + for (int j = 0; j < n; j++) + for (int k = 0; k < m; k++) + c[i][j] += a[i][k] * b[k][j]; + return c; +} +``` +```java +public static int[][] matrixMultiplicationNLM(int l, int m, int n, int[][] a, int[][] b) { + int[][] c = new int[l][n]; + for (int j = 0; j < n; j++) + for (int i = 0; i < l; i++) + for (int k = 0; k < m; k++) + c[i][j] += a[i][k] * b[k][j]; + return c; +} +``` +```java +public static int[][] matrixMultiplicationMNL(int l, int m, int n, int[][] a, int[][] b) { + int[][] c = new int[l][n]; + for (int k = 0; k < m; k++) + for (int j = 0; j < n; j++) + for (int i = 0; i < l; i++) + c[i][j] += a[i][k] * b[k][j]; + return c; +} +``` +```java +public static int[][] matrixMultiplicationNML(int l, int m, int n, int[][] a, int[][] b) { + int[][] c = new int[l][n]; + for (int j = 0; j < n; j++) + for (int k = 0; k < m; k++) + for (int i = 0; i < l; i++) + c[i][j] += a[i][k] * b[k][j]; + return c; +} +``` +{% endcapture %} +{%- include collapsed_block.html summary="Код без комментариев" content=collapsed_md -%} + +## Сравнение алгоритмов {#comparing-algorithms} + +Для проверки возьмём две матрицы `A=[500×700]` и `B=[700×450]`, заполненные случайными числами. +Сначала сравниваем между собой корректность реализации алгоритмов — все полученные результаты +должны совпадать. Затем выполняем каждый метод по 10 раз и подсчитываем среднее время выполнения. + +```java +// запускаем программу и выводим результат +public static void main(String[] args) throws Exception { + // входящие данные + int l = 500, m = 700, n = 450, steps = 10; + int[][] a = randomMatrix(l, m), b = randomMatrix(m, n); + // карта методов для сравнения + var methods = new TreeMap>(Map.of( + "LMN", () -> matrixMultiplicationLMN(l, m, n, a, b), + "LNM", () -> matrixMultiplicationLNM(l, m, n, a, b), + "MLN", () -> matrixMultiplicationMLN(l, m, n, a, b), + "MNL", () -> matrixMultiplicationMNL(l, m, n, a, b), + "NLM", () -> matrixMultiplicationNLM(l, m, n, a, b), + "NML", () -> matrixMultiplicationNML(l, m, n, a, b))); + int[][] last = null; + // обходим карту методов, проверяем корректность результатов, + // все полученные результаты должны быть равны друг другу + for (var method : methods.entrySet()) { + // следующий метод для сравнения + var next = methods.higherEntry(method.getKey()); + // если текущий метод не последний — сравниваем результаты двух методов + if (next != null) System.out.println(method.getKey() + "=" + next.getKey() + ": " + // сравниваем результат выполнения текущего метода и следующего за ним + + Arrays.deepEquals(method.getValue().call(), next.getValue().call())); + // результат выполнения последнего метода + else last = method.getValue().call(); + } + int[][] test = last; + // обходим карту методов, замеряем время работы каждого метода + for (var method : methods.entrySet()) + // параметры: заголовок, количество шагов, исполняемый код + benchmark(method.getKey(), steps, () -> { + try { // выполняем метод, получаем результат + int[][] result = method.getValue().call(); + // проверяем корректность результатов на каждом шаге + if (!Arrays.deepEquals(result, test)) System.out.print("error"); + } catch (Exception e) { + e.printStackTrace(); + } + }); +} +``` + +{% capture collapsed_md %} +```java +// вспомогательный метод, возвращает матрицу указанного размера +private static int[][] randomMatrix(int row, int col) { + int[][] matrix = new int[row][col]; + for (int i = 0; i < row; i++) + for (int j = 0; j < col; j++) + matrix[i][j] = (int) (Math.random() * row * col); + return matrix; +} +``` +```java +// вспомогательный метод для замера времени работы переданного кода +private static void benchmark(String title, int steps, Runnable runnable) { + long time, avg = 0; + System.out.print(title); + for (int i = 0; i < steps; i++) { + time = System.currentTimeMillis(); + runnable.run(); + time = System.currentTimeMillis() - time; + // время выполнения одного шага + System.out.print(" | " + time); + avg += time; + } + // среднее время выполнения + System.out.println(" || " + (avg / steps)); +} +``` +{% endcapture %} +{%- include collapsed_block.html summary="Вспомогательные методы" content=collapsed_md -%} + +Вывод зависит от среды выполнения, время в миллисекундах: + +``` +LMN=LNM: true +LNM=MLN: true +MLN=MNL: true +MNL=NLM: true +NLM=NML: true +LMN | 191 | 109 | 105 | 106 | 105 | 106 | 106 | 105 | 123 | 109 || 116 +LNM | 417 | 418 | 419 | 416 | 416 | 417 | 418 | 417 | 416 | 417 || 417 +MLN | 113 | 115 | 113 | 115 | 114 | 114 | 114 | 115 | 114 | 113 || 114 +MNL | 857 | 864 | 857 | 859 | 860 | 863 | 862 | 860 | 858 | 860 || 860 +NLM | 404 | 404 | 407 | 404 | 406 | 405 | 405 | 404 | 403 | 404 || 404 +NML | 866 | 872 | 867 | 868 | 867 | 868 | 867 | 873 | 869 | 863 || 868 +``` + +Все описанные выше методы, включая свёрнутые блоки, можно поместить в одном классе. + +{% capture collapsed_md %} +```java +import java.util.Arrays; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.Callable; +``` +{% endcapture %} +{%- include collapsed_block.html summary="Необходимые импорты" content=collapsed_md -%} diff --git a/jekyll_site/ru/2021/12/12/matrix-rotation-90-degrees.md b/jekyll_site/ru/2021/12/12/matrix-rotation-90-degrees.md new file mode 100644 index 0000000..0c57a95 --- /dev/null +++ b/jekyll_site/ru/2021/12/12/matrix-rotation-90-degrees.md @@ -0,0 +1,96 @@ +--- +title: Поворот матрицы на 90 градусов +description: Рассмотрим алгоритм поворота матрицы на 90 градусов по часовой стрелке и против часовой стрелки. Этот алгоритм похож на транспонирование матрицы с тем... +sections: [Транспонирование,Сравнение алгоритмов] +tags: [java,массивы,многомерные массивы,матрицы,строки,колонки,циклы,вложенные циклы] +canonical_url: /ru/2021/12/12/matrix-rotation-90-degrees.html +url_translated: /en/2021/12/13/matrix-rotation-90-degrees.html +title_translated: Matrix rotation 90 degrees +date: 2021.12.12 +--- + +Рассмотрим алгоритм поворота матрицы на 90 градусов по часовой стрелке и против часовой стрелки. +Этот алгоритм похож на *транспонирование матрицы* с тем отличием, что здесь для каждой точки +одна из координат отображается зеркально. + +```java +// транспонирование матрицы — строки и колонки меняются местами +swapped[j][i] = matrix[i][j]; +// по часовой стрелке ↻ — строки отображаются зеркально +rotated[j][i] = matrix[m-i-1][j]; +// против часовой стрелки ↺ — колонки отображаются зеркально +rotated[j][i] = matrix[i][n-j-1]; +``` + +*Похожий алгоритм: [Поворот матрицы на 180 градусов]({{ '/ru/2021/12/16/matrix-rotation-180-degrees.html' | relative_url }}).* + +Напишем метод на Java для поворота матрицы {`m×n`} на 90 градусов. Дополнительный параметр задаёт +направление поворота: по часовой стрелке или против часовой стрелки. Для примера возьмём +прямоугольную матрицу {`4×3`}. + +```java +/** + * @param m количество строк исходной матрицы + * @param n количество колонок исходной матрицы + * @param clock направление поворота: по часовой + * стрелке ↻ или против часовой стрелки ↺ + * @param matrix исходная матрица + * @return повёрнутая матрица + */ +public static int[][] rotateMatrix(int m, int n, boolean clock, int[][] matrix) { + // новая матрица, количества строк и колонок меняются местами + int[][] rotated = new int[n][m]; + // обходим строки исходной матрицы + for (int i = 0; i < m; i++) + // обходим колонки исходной матрицы + for (int j = 0; j < n; j++) + if (clock) // поворот по часовой стрелке ↻ + rotated[j][i] = matrix[m-i-1][j]; + else // поворот против часовой стрелки ↺ + rotated[j][i] = matrix[i][n-j-1]; + return rotated; +} +``` +{% raw %} +```java +// запускаем программу и выводим результат +public static void main(String[] args) { + // исходные данные + int m = 4, n = 3; + int[][] matrix = {{11, 12, 13}, {14, 15, 16}, {17, 18, 19}, {20, 21, 22}}; + // поворачиваем матрицу и выводим результат + outputMatrix("Исходная матрица:", matrix); + outputMatrix("По часовой стрелке ↻:", rotateMatrix(m, n, true, matrix)); + outputMatrix("Против часовой стрелки ↺:", rotateMatrix(m, n, false, matrix)); +} +``` +{% endraw %} +```java +// вспомогательный метод, выводит матрицу в консоль построчно +public static void outputMatrix(String title, int[][] matrix) { + System.out.println(title); + for (int[] row : matrix) { + for (int el : row) + System.out.print(" " + el); + System.out.println(); + } +} +``` + +Вывод: + +``` +Исходная матрица: + 11 12 13 + 14 15 16 + 17 18 19 + 20 21 22 +По часовой стрелке ↻: + 20 17 14 11 + 21 18 15 12 + 22 19 16 13 +Против часовой стрелки ↺: + 13 16 19 22 + 12 15 18 21 + 11 14 17 20 +``` diff --git a/jekyll_site/ru/2021/12/16/matrix-rotation-180-degrees.md b/jekyll_site/ru/2021/12/16/matrix-rotation-180-degrees.md new file mode 100644 index 0000000..7ba6f47 --- /dev/null +++ b/jekyll_site/ru/2021/12/16/matrix-rotation-180-degrees.md @@ -0,0 +1,84 @@ +--- +title: Поворот матрицы на 180 градусов +description: Рассмотрим алгоритм разворота матрицы на 180 градусов. В отличие от алгоритма транспонирования, здесь в результирующей матрице строки и колонки не меняются... +sections: [Транспонирование,Сравнение алгоритмов] +tags: [java,массивы,многомерные массивы,матрицы,строки,колонки,циклы,вложенные циклы] +canonical_url: /ru/2021/12/16/matrix-rotation-180-degrees.html +url_translated: /en/2021/12/17/matrix-rotation-180-degrees.html +title_translated: Matrix rotation 180 degrees +date: 2021.12.16 +--- + +Рассмотрим алгоритм разворота матрицы на 180 градусов. В отличие от алгоритма *транспонирования*, +здесь в результирующей матрице строки и колонки не меняются местами, но отображаются зеркально. + +```java +// строки и колонки меняются местами +swapped[j][i] = matrix[i][j]; +// строки и колонки отображаются зеркально +rotated[i][j] = matrix[m-i-1][n-j-1]; +``` + +*Похожий алгоритм: [Поворот матрицы на 90 градусов]({{ '/ru/2021/12/12/matrix-rotation-90-degrees.html' | relative_url }}).* + +Напишем метод на Java для поворота матрицы {`m×n`} на 180 градусов. Для примера возьмём +прямоугольную матрицу {`4×3`}. + +```java +/** + * @param m количество строк исходной матрицы + * @param n количество колонок исходной матрицы + * @param matrix исходная матрица + * @return повёрнутая матрица + */ +public static int[][] rotateMatrix(int m, int n, int[][] matrix) { + // новая матрица + int[][] rotated = new int[m][n]; + // обходим строки исходной матрицы + for (int i = 0; i < m; i++) + // обходим колонки исходной матрицы + for (int j = 0; j < n; j++) + // строки и колонки отображаются зеркально + rotated[i][j] = matrix[m-i-1][n-j-1]; + return rotated; +} +``` +{% raw %} +```java +// запускаем программу и выводим результат +public static void main(String[] args) { + // исходные данные + int m = 4, n = 3; + int[][] matrix = {{11, 12, 13}, {14, 15, 16}, {17, 18, 19}, {20, 21, 22}}; + // поворачиваем матрицу и выводим результат + outputMatrix("Исходная матрица:", matrix); + outputMatrix("Поворот на 180°:", rotateMatrix(m, n, matrix)); +} +``` +{% endraw %} +```java +// вспомогательный метод, выводит матрицу в консоль построчно +public static void outputMatrix(String title, int[][] matrix) { + System.out.println(title); + for (int[] row : matrix) { + for (int el : row) + System.out.print(" " + el); + System.out.println(); + } +} +``` + +Вывод: + +``` +Исходная матрица: + 11 12 13 + 14 15 16 + 17 18 19 + 20 21 22 +Поворот на 180°: + 22 21 20 + 19 18 17 + 16 15 14 + 13 12 11 +``` diff --git a/jekyll_site/ru/2022/02/08/matrix-multiplication-parallel-streams.md b/jekyll_site/ru/2022/02/08/matrix-multiplication-parallel-streams.md new file mode 100644 index 0000000..6c0b3fb --- /dev/null +++ b/jekyll_site/ru/2022/02/08/matrix-multiplication-parallel-streams.md @@ -0,0 +1,182 @@ +--- +title: Умножение матриц в параллельных потоках +description: Рассмотрим алгоритм перемножения прямоугольных матриц с использованием потоков Java Stream. Возьмём оптимизированный вариант алгоритма на трёх вложенных... +sections: [Многопоточность,Вложенные циклы,Сравнение алгоритмов] +tags: [java,потоки,массивы,многомерные массивы,матрицы,строки,циклы] +canonical_url: /ru/2022/02/08/matrix-multiplication-parallel-streams.html +url_translated: /en/2022/02/09/matrix-multiplication-parallel-streams.html +title_translated: Matrix multiplication in parallel streams +date: 2022.02.08 +--- + +Рассмотрим алгоритм перемножения прямоугольных матриц с использованием потоков Java Stream. Возьмём +*оптимизированный вариант* алгоритма на трёх вложенных циклах и заменим внешний цикл на поток. Сравним +время работы двух алгоритмов. + +Строки результирующей матрицы обрабатываем в параллельном режиме, а каждую строку заполняем слоями. +За счёт параллельного использования кеша среды выполнения на многоядерных машинах время вычислений +можно сократить более чем в два раза. Для проверки возьмём две прямоугольные матрицы: {`L×M`} и {`M×N`}. + +*Дальнейшая оптимизация: [Алгоритм Винограда — Штрассена]({{ '/ru/2022/02/10/winograd-strassen-algorithm.html' | relative_url }}).* + +## Параллельный поток {#parallel-stream} + +Строки первой матрицы `L` обходим в параллельном режиме. Внутри каждого потока два вложенных цикла: +по *общей стороне* двух матриц `M` и по колонкам второй матрицы `N`. Обработка строк результирующей +матрицы происходит независимо друг от друга в параллельных потоках. + +```java +/** + * @param l строки матрицы 'a' + * @param m колонки матрицы 'a' + * и строки матрицы 'b' + * @param n колонки матрицы 'b' + * @param a первая матрица 'l×m' + * @param b вторая матрица 'm×n' + * @return результирующая матрица 'l×n' + */ +public static int[][] parallelMatrixMultiplication(int l, int m, int n, int[][] a, int[][] b) { + // результирующая матрица + int[][] c = new int[l][n]; + // обходим индексы строк матрицы 'a' в параллельном режиме + IntStream.range(0, l).parallel().forEach(i -> { + // обходим индексы общей стороны двух матриц: + // колонок матрицы 'a' и строк матрицы 'b' + for (int k = 0; k < m; k++) + // обходим индексы колонок матрицы 'b' + for (int j = 0; j < n; j++) + // сумма произведений элементов i-ой строки + // матрицы 'a' и j-ой колонки матрицы 'b' + c[i][j] += a[i][k] * b[k][j]; + }); + return c; +} +``` + +## Три вложенных цикла {#three-nested-loops} + +Возьмём оптимизированный вариант алгоритма, который лучше прочих использует кеш среды выполнения. +Внешний цикл обходит строки первой матрицы `L`, далее идёт цикл по *общей стороне* двух матриц `M` +и за ним цикл по колонкам второй матрицы `N`. Запись в результирующую матрицу происходит построчно, +а каждая строка заполняется слоями. + +```java +/** + * @param l строки матрицы 'a' + * @param m колонки матрицы 'a' + * и строки матрицы 'b' + * @param n колонки матрицы 'b' + * @param a первая матрица 'l×m' + * @param b вторая матрица 'm×n' + * @return результирующая матрица 'l×n' + */ +public static int[][] sequentialMatrixMultiplication(int l, int m, int n, int[][] a, int[][] b) { + // результирующая матрица + int[][] c = new int[l][n]; + // обходим индексы строк матрицы 'a' + for (int i = 0; i < l; i++) + // обходим индексы общей стороны двух матриц: + // колонок матрицы 'a' и строк матрицы 'b' + for (int k = 0; k < m; k++) + // обходим индексы колонок матрицы 'b' + for (int j = 0; j < n; j++) + // сумма произведений элементов i-ой строки + // матрицы 'a' и j-ой колонки матрицы 'b' + c[i][j] += a[i][k] * b[k][j]; + return c; +} +``` + +## Тестирование {#testing} + +Для проверки возьмём две матрицы: `A=[900×1000]` и `B=[1000×750]`, заполненные случайными числами. +Сначала сравниваем между собой корректность реализации двух алгоритмов — произведения матриц +должны совпадать. Затем выполняем каждый метод по 10 раз и подсчитываем среднее время выполнения. + +```java +// запускаем программу и выводим результат +public static void main(String[] args) { + // входящие данные + int l = 900, m = 1000, n = 750, steps = 10; + int[][] a = randomMatrix(l, m), b = randomMatrix(m, n); + // произведения матриц + int[][] c1 = parallelMatrixMultiplication(l, m, n, a, b); + int[][] c2 = sequentialMatrixMultiplication(l, m, n, a, b); + // проверяем корректность результатов + System.out.println("Результаты совпадают: " + Arrays.deepEquals(c1, c2)); + // замеряем время работы двух методов + benchmark("Потоки", steps, () -> { + int[][] c = parallelMatrixMultiplication(l, m, n, a, b); + if (!Arrays.deepEquals(c, c1)) System.out.print("ошибка"); + }); + benchmark("Циклы", steps, () -> { + int[][] c = sequentialMatrixMultiplication(l, m, n, a, b); + if (!Arrays.deepEquals(c, c2)) System.out.print("ошибка"); + }); +} +``` + +{% capture collapsed_md %} +```java +// вспомогательный метод, возвращает матрицу указанного размера +private static int[][] randomMatrix(int row, int col) { + int[][] matrix = new int[row][col]; + for (int i = 0; i < row; i++) + for (int j = 0; j < col; j++) + matrix[i][j] = (int) (Math.random() * row * col); + return matrix; +} +``` +```java +// вспомогательный метод для замера времени работы переданного кода +private static void benchmark(String title, int steps, Runnable runnable) { + long time, avg = 0; + System.out.print(title); + for (int i = 0; i < steps; i++) { + time = System.currentTimeMillis(); + runnable.run(); + time = System.currentTimeMillis() - time; + // время выполнения одного шага + System.out.print(" | " + time); + avg += time; + } + // среднее время выполнения + System.out.println(" || " + (avg / steps)); +} +``` +{% endcapture %} +{%- include collapsed_block.html summary="Вспомогательные методы" content=collapsed_md -%} + +Вывод зависит от среды выполнения, время в миллисекундах: + +``` +Результаты совпадают: true +Потоки | 113 | 144 | 117 | 114 | 114 | 117 | 120 | 125 | 111 | 113 || 118 +Циклы | 1357 | 530 | 551 | 569 | 535 | 538 | 525 | 517 | 518 | 514 || 615 +``` + +## Сравнение алгоритмов {#comparing-algorithms} + +На восьмиядерном компьютере Linux x64 создаём для тестов виртуальную машину Windows x64. При +прочих равных условиях в настройках меняем количество процессоров. Запускаем вышеописанный +тест 100 раз вместо 10 — получаем сводную таблицу результатов. Время в миллисекундах. + +``` + ЦПУ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | +-------|-----|-----|-----|-----|-----|-----|-----|-----| +Потоки | 522 | 276 | 179 | 154 | 128 | 112 | 110 | 93 | +Циклы | 519 | 603 | 583 | 571 | 545 | 571 | 559 | 467 | +``` + +Результаты: при большом количестве процессоров в системе многопоточный алгоритм отрабатывает +заметно быстрее, чем три вложенных цикла. Время вычислений можно сократить более чем в два раза. + +Все описанные выше методы, включая свёрнутые блоки, можно поместить в одном классе. + +{% capture collapsed_md %} +```java +import java.util.Arrays; +import java.util.stream.IntStream; +``` +{% endcapture %} +{%- include collapsed_block.html summary="Необходимые импорты" content=collapsed_md -%} diff --git a/jekyll_site/ru/2022/02/10/winograd-strassen-algorithm.md b/jekyll_site/ru/2022/02/10/winograd-strassen-algorithm.md new file mode 100644 index 0000000..845646f --- /dev/null +++ b/jekyll_site/ru/2022/02/10/winograd-strassen-algorithm.md @@ -0,0 +1,293 @@ +--- +title: Алгоритм Винограда — Штрассена +description: Рассмотрим модификацию алгоритма Штрассена для умножения квадратных матриц с меньшим количеством сложений между блоками, чем в обычном алгоритме — 15 вместо... +sections: [Многопоточность,Блочные матрицы,Сравнение алгоритмов] +tags: [java,потоки,массивы,многомерные массивы,матрицы,рекурсия,циклы,вложенные циклы] +canonical_url: /ru/2022/02/10/winograd-strassen-algorithm.html +url_translated: /en/2022/02/11/winograd-strassen-algorithm.html +title_translated: Winograd — Strassen algorithm +date: 2022.02.10 +--- + +Рассмотрим модификацию алгоритма Штрассена для умножения квадратных матриц с *меньшим* количеством +сложений между блоками, чем в обычном алгоритме — 15 вместо 18 и таким же количеством умножений как +в обычном алгоритме — 7. Будем использовать потоки Java Stream. + +Рекурсивное дробление матриц на блоки при перемножении имеет смысл до определенного предела, а дальше +теряет смысл, так как алгоритм Штрассена не использует кеш среды выполнения. Поэтому для малых блоков +будем использовать параллельный вариант вложенных циклов, а для больших блоков параллельно будем +выполнять рекурсивное дробление. + +Границу между двумя алгоритмами определяем экспериментально — подстраиваем под кеш среды выполнения. +Выгода алгоритма Штрассена становится заметнее на больших матрицах — отличие от алгоритма на вложенных +циклах становится больше и зависит от среды выполнения. Сравним время работы двух алгоритмов. + +*Алгоритм на трёх вложенных циклах: [Оптимизация умножения матриц]({{ '/ru/2021/12/09/optimizing-matrix-multiplication.html' | relative_url }}).* + +## Описание алгоритма {#algorithm-description} + +Матрицы должны быть одинакового размера. Разделяем каждую матрицу на 4 равных блока. Блоки должны быть +квадратными, поэтому если это не так, тогда сначала дополняем матрицы нулевыми строками и столбцами, +а после этого разделяем на блоки. Лишние строки и столбцы потом уберём из результирующей матрицы. + +{% include image_svg.html src="/img/block-matrices.svg" style="width:221pt; height:33pt;" +alt="{\displaystyle A={\begin{pmatrix}A_{11}&A_{12}\\A_{21}&A_{22}\end{pmatrix}},\quad B={\begin{pmatrix}B_{11}&B_{12}\\B_{21}&B_{22}\end{pmatrix}}.}" %} + +Суммируем блоки. + +{% include image_svg.html src="/img/sums1.svg" style="width:101pt; height:148pt;" +alt="{\displaystyle{\begin{aligned}S_{1}&=(A_{21}+A_{22});\\S_{2}&=(S_{1}-A_{11});\\S_{3}&=(A_{11}-A_{21});\\S_{4}&=(A_{12}-S_{2});\\S_{5}&=(B_{12}-B_{11});\\S_{6}&=(B_{22}-S_{5});\\S_{7}&=(B_{22}-B_{12});\\S_{8}&=(S_{6}-B_{21}).\end{aligned}}}" %} + +Умножаем блоки. + +{% include image_svg.html src="/img/products.svg" style="width:75pt; height:127pt;" +alt="{\displaystyle{\begin{aligned}P_{1}&=S_{2}S_{6};\\P_{2}&=A_{11}B_{11};\\P_{3}&=A_{12}B_{21};\\P_{4}&=S_{3}S_{7};\\P_{5}&=S_{1}S_{5};\\P_{6}&=S_{4}B_{22};\\P_{7}&=A_{22}S_{8}.\end{aligned}}}" %} + +Суммируем блоки. + +{% include image_svg.html src="/img/sums2.svg" style="width:78pt; height:31pt;" +alt="{\displaystyle{\begin{aligned}T_{1}&=P_{1}+P_{2};\\T_{2}&=T_{1}+P_{4}.\end{aligned}}}" %} + +Блоки результирующей матрицы. + +{% include image_svg.html src="/img/sums3.svg" style="width:240pt; height:33pt;" +alt="{\displaystyle{\begin{pmatrix}C_{11}&C_{12}\\C_{21}&C_{22}\end{pmatrix}}={\begin{pmatrix}P_{2}+P_{3}&T_{1}+P_{5}+P_{6}\\T_{2}-P_{7}&T_{2}+P_{5}\end{pmatrix}}.}" %} + +## Гибридный алгоритм {#hybrid-algorithm} + +Каждую матрицу `A` и `B` делим на 4 *равных* блока, при необходимости дополняем недостающие части +нулями. Выполняем 15 сложений и 7 умножений над блоками — получаем 4 блока матрицы `C`. Убираем +лишние нули, если добавляли, и возвращаем результирующую матрицу. Рекурсивное дробление больших +блоков запускаем в параллельном режиме, а для малых блоков вызываем алгоритм на вложенных циклах. + +```java +/** + * @param n размер матрицы + * @param brd минимальный размер матрицы + * @param a первая матрица 'n×n' + * @param b вторая матрица 'n×n' + * @return результирующая матрица 'n×n' + */ +public static int[][] multiplyMatrices(int n, int brd, int[][] a, int[][] b) { + // малые блоки перемножаем с помощью алгоритма на вложенных циклах + if (n < brd) return simpleMultiplication(n, a, b); + // серединная точка матрицы, округляем в большую сторону — блоки должны + // быть квадратными, при необходимости добавляем нулевые строки и столбцы + int m = n - n / 2; + // блоки первой матрицы + int[][] a11 = getQuadrant(m, n, a, true, true); + int[][] a12 = getQuadrant(m, n, a, true, false); + int[][] a21 = getQuadrant(m, n, a, false, true); + int[][] a22 = getQuadrant(m, n, a, false, false); + // блоки второй матрицы + int[][] b11 = getQuadrant(m, n, b, true, true); + int[][] b12 = getQuadrant(m, n, b, true, false); + int[][] b21 = getQuadrant(m, n, b, false, true); + int[][] b22 = getQuadrant(m, n, b, false, false); + // суммируем блоки + int[][] s1 = sumMatrices(m, a21, a22, true); + int[][] s2 = sumMatrices(m, s1, a11, false); + int[][] s3 = sumMatrices(m, a11, a21, false); + int[][] s4 = sumMatrices(m, a12, s2, false); + int[][] s5 = sumMatrices(m, b12, b11, false); + int[][] s6 = sumMatrices(m, b22, s5, false); + int[][] s7 = sumMatrices(m, b22, b12, false); + int[][] s8 = sumMatrices(m, s6, b21, false); + int[][][] p = new int[7][][]; + // перемножаем блоки в параллельных потоках + IntStream.range(0, 7).parallel().forEach(i -> { + switch (i) { // рекурсивные вызовы + case 0: p[i] = multiplyMatrices(m, brd, s2, s6); break; + case 1: p[i] = multiplyMatrices(m, brd, a11, b11); break; + case 2: p[i] = multiplyMatrices(m, brd, a12, b21); break; + case 3: p[i] = multiplyMatrices(m, brd, s3, s7); break; + case 4: p[i] = multiplyMatrices(m, brd, s1, s5); break; + case 5: p[i] = multiplyMatrices(m, brd, s4, b22); break; + case 6: p[i] = multiplyMatrices(m, brd, a22, s8); break; + } + }); + // суммируем блоки + int[][] t1 = sumMatrices(m, p[0], p[1], true); + int[][] t2 = sumMatrices(m, t1, p[3], true); + // блоки результирующей матрицы + int[][] c11 = sumMatrices(m, p[1], p[2], true); + int[][] c12 = sumMatrices(m, t1, sumMatrices(m, p[4], p[5], true), true); + int[][] c21 = sumMatrices(m, t2, p[6], false); + int[][] c22 = sumMatrices(m, t2, p[4], true); + // собираем результирующую матрицу из блоков, + // убираем нулевые строки и столбцы, если добавляли + return putQuadrants(m, n, c11, c12, c21, c22); +} +``` + +{% capture collapsed_md %} +```java +// вспомогательный метод для суммирования матриц +private static int[][] sumMatrices(int n, int[][] a, int[][] b, boolean sign) { + int[][] c = new int[n][n]; + for (int i = 0; i < n; i++) + for (int j = 0; j < n; j++) + c[i][j] = sign ? a[i][j] + b[i][j] : a[i][j] - b[i][j]; + return c; +} +``` +```java +// вспомогательный метод, получает блок матрицы +private static int[][] getQuadrant(int m, int n, int[][] x, + boolean first, boolean second) { + int[][] q = new int[m][m]; + if (first) for (int i = 0; i < m; i++) + if (second) System.arraycopy(x[i], 0, q[i], 0, m); // x11 + else System.arraycopy(x[i], m, q[i], 0, n - m); // x12 + else for (int i = m; i < n; i++) + if (second) System.arraycopy(x[i], 0, q[i - m], 0, m); // x21 + else System.arraycopy(x[i], m, q[i - m], 0, n - m); // x22 + return q; +} +``` +```java +// вспомогательный метод, собирает матрицу из блоков +private static int[][] putQuadrants(int m, int n, + int[][] x11, int[][] x12, + int[][] x21, int[][] x22) { + int[][] x = new int[n][n]; + for (int i = 0; i < n; i++) + if (i < m) { + System.arraycopy(x11[i], 0, x[i], 0, m); + System.arraycopy(x12[i], 0, x[i], m, n - m); + } else { + System.arraycopy(x21[i - m], 0, x[i], 0, m); + System.arraycopy(x22[i - m], 0, x[i], m, n - m); + } + return x; +} +``` +{% endcapture %} +{%- include collapsed_block.html summary="Вспомогательные методы" content=collapsed_md -%} + +## Вложенные циклы {#nested-loops} + +Для дополнения предыдущего алгоритма и для сравнения с ним же будем использовать *оптимизированный* +вариант вложенных циклов, который лучше прочих использует кеш среды выполнения — обработка строк +результирующей матрицы выполняется независимо друг от друга в параллельных потоках. Для малых матриц +будем использовать этот алгоритм — большие матрицы делим на малые блоки и используем этот же алгоритм. + +```java +/** + * @param n размер матрицы + * @param a первая матрица 'n×n' + * @param b вторая матрица 'n×n' + * @return результирующая матрица 'n×n' + */ +public static int[][] simpleMultiplication(int n, int[][] a, int[][] b) { + // результирующая матрица + int[][] c = new int[n][n]; + // обходим индексы строк матрицы 'a' в параллельном режиме + IntStream.range(0, n).parallel().forEach(i -> { + // обходим индексы общей стороны двух матриц: + // колонок матрицы 'a' и строк матрицы 'b' + for (int k = 0; k < n; k++) + // обходим индексы колонок матрицы 'b' + for (int j = 0; j < n; j++) + // сумма произведений элементов i-ой строки + // матрицы 'a' и j-ой колонки матрицы 'b' + c[i][j] += a[i][k] * b[k][j]; + }); + return c; +} +``` + +## Тестирование {#testing} + +Для проверки возьмём две квадратные матрицы `A=[1000×1000]` и `B=[1000×1000]`, заполненные случайными +числами. Минимальный размер блока возьмём `[200×200]` элементов. Сначала сравниваем между собой +корректность реализации двух алгоритмов — произведения матриц должны совпадать. Затем выполняем каждый +метод по 10 раз и подсчитываем среднее время выполнения. + +```java +// запускаем программу и выводим результат +public static void main(String[] args) { + // входящие данные + int n = 1000, brd = 200, steps = 10; + int[][] a = randomMatrix(n, n), b = randomMatrix(n, n); + // произведения матриц + int[][] c1 = multiplyMatrices(n, brd, a, b); + int[][] c2 = simpleMultiplication(n, a, b); + // проверяем корректность результатов + System.out.println("Результаты совпадают: " + Arrays.deepEquals(c1, c2)); + // замеряем время работы двух методов + benchmark("Гибридный алгоритм", steps, () -> { + int[][] c = multiplyMatrices(n, brd, a, b); + if (!Arrays.deepEquals(c, c1)) System.out.print("ошибка"); + }); + benchmark("Вложенные циклы ", steps, () -> { + int[][] c = simpleMultiplication(n, a, b); + if (!Arrays.deepEquals(c, c2)) System.out.print("ошибка"); + }); +} +``` + +{% capture collapsed_md %} +```java +// вспомогательный метод, возвращает матрицу указанного размера +private static int[][] randomMatrix(int row, int col) { + int[][] matrix = new int[row][col]; + for (int i = 0; i < row; i++) + for (int j = 0; j < col; j++) + matrix[i][j] = (int) (Math.random() * row * col); + return matrix; +} +``` +```java +// вспомогательный метод для замера времени работы переданного кода +private static void benchmark(String title, int steps, Runnable runnable) { + long time, avg = 0; + System.out.print(title); + for (int i = 0; i < steps; i++) { + time = System.currentTimeMillis(); + runnable.run(); + time = System.currentTimeMillis() - time; + // время выполнения одного шага + System.out.print(" | " + time); + avg += time; + } + // среднее время выполнения + System.out.println(" || " + (avg / steps)); +} +``` +{% endcapture %} +{%- include collapsed_block.html summary="Вспомогательные методы" content=collapsed_md -%} + +Вывод зависит от среды выполнения, время в миллисекундах: + +``` +Результаты совпадают: true +Гибридный алгоритм | 196 | 177 | 156 | 205 | 154 | 165 | 133 | 118 | 132 | 134 || 157 +Вложенные циклы | 165 | 164 | 168 | 167 | 168 | 168 | 170 | 179 | 173 | 168 || 169 +``` + +## Сравнение алгоритмов {#comparing-algorithms} + +На восьмиядерном компьютере Linux x64 запускаем вышеописанный тест 100 раз вместо 10. Минимальный +размер блока берём `[brd=200]`. Изменяем только `n` — размеры обеих матриц `A=[n×n]` и `B=[n×n]`. +Получаем сводную таблицу результатов. Время в миллисекундах. + +``` + n | 900 | 1000 | 1100 | 1200 | 1300 | 1400 | 1500 | 1600 | 1700 | +-------------------|-----|------|------|------|------|------|------|------|------| +Гибридный алгоритм | 96 | 125 | 169 | 204 | 260 | 313 | 384 | 482 | 581 | +Вложенные циклы | 119 | 162 | 235 | 281 | 361 | 497 | 651 | 793 | 971 | +``` + +Результаты: выгода алгоритма Штрассена становится заметнее на больших матрицах, когда размер +самой матрицы в несколько раз превышает размер минимального блока, и зависит от среды выполнения. + +Все описанные выше методы, включая свёрнутые блоки, можно поместить в одном классе. + +{% capture collapsed_md %} +```java +import java.util.Arrays; +import java.util.stream.IntStream; +``` +{% endcapture %} +{%- include collapsed_block.html summary="Необходимые импорты" content=collapsed_md -%} diff --git a/jekyll_site/ru/index.md b/jekyll_site/ru/index.md new file mode 100644 index 0000000..f38e77e --- /dev/null +++ b/jekyll_site/ru/index.md @@ -0,0 +1,73 @@ +--- +title: Код с комментариями +description: Заметки на тему программирования с примерами кода и комментариями. Решения задач и описания решений. +sections: [Решения задач и описания решений] +tags: [java,алгоритмы,реализация,массивы,многомерные массивы,матрицы,циклы,потоки] +canonical_url: / +url_translated: /en/ +title_translated: Code with comments +--- + +{%- assign articles = "" | split: "" %} +{%- assign articles = articles | push: "Алгоритм Винограда — Штрассена" %} +{%- capture article_brief %} +Рассмотрим модификацию алгоритма Штрассена для умножения квадратных матриц с *меньшим* количеством +сложений между блоками, чем в обычном алгоритме — 15 вместо 18 и таким же количеством умножений как +в обычном алгоритме — 7. Будем использовать потоки Java Stream. + +Рекурсивное дробление матриц на блоки при перемножении имеет смысл до определенного предела, а дальше +теряет смысл, так как алгоритм Штрассена не использует кеш среды выполнения. Поэтому для малых блоков +будем использовать параллельный вариант вложенных циклов, а для больших блоков параллельно будем +выполнять рекурсивное дробление. + +Границу между двумя алгоритмами определяем экспериментально — подстраиваем под кеш среды выполнения. +Выгода алгоритма Штрассена становится заметнее на больших матрицах — отличие от алгоритма на вложенных +циклах становится больше и зависит от среды выполнения. Сравним время работы двух алгоритмов. +{%- endcapture %} +{%- assign articles = articles | push: article_brief %} +{%- assign articles = articles | push: "Умножение матриц в параллельных потоках" %} +{%- capture article_brief %} +Рассмотрим алгоритм перемножения прямоугольных матриц с использованием потоков Java Stream. Возьмём +*оптимизированный вариант* алгоритма на трёх вложенных циклах и заменим внешний цикл на поток. Сравним +время работы двух алгоритмов. + +Строки результирующей матрицы обрабатываем в параллельном режиме, а каждую строку заполняем слоями. +За счёт параллельного использования кеша среды выполнения на многоядерных машинах время вычислений +можно сократить более чем в два раза. Для проверки возьмём две прямоугольные матрицы: {`L×M`} и {`M×N`}. +{%- endcapture %} +{%- assign articles = articles | push: article_brief %} +{%- assign articles = articles | push: "Поворот матрицы на 180 градусов" %} +{%- capture article_brief %} +Рассмотрим алгоритм разворота матрицы на 180 градусов. В отличие от алгоритма *транспонирования*, +здесь в результирующей матрице строки и колонки не меняются местами, но отображаются зеркально. + +Напишем метод на Java для поворота матрицы {`m×n`} на 180 градусов. Для примера возьмём +прямоугольную матрицу {`4×3`}. +{%- endcapture %} +{%- assign articles = articles | push: article_brief %} +{%- assign articles = articles | push: "Поворот матрицы на 90 градусов" %} +{%- capture article_brief %} +Рассмотрим алгоритм поворота матрицы на 90 градусов по часовой стрелке и против часовой стрелки. +Этот алгоритм похож на *транспонирование матрицы* с тем отличием, что здесь для каждой точки +одна из координат отображается зеркально. + +Напишем метод на Java для поворота матрицы {`m×n`} на 90 градусов. Дополнительный параметр задаёт +направление поворота: по часовой стрелке или против часовой стрелки. Для примера возьмём +прямоугольную матрицу {`4×3`}. +{%- endcapture %} +{%- assign articles = articles | push: article_brief %} +{%- assign articles = articles | push: "Оптимизация умножения матриц" %} +{%- capture article_brief %} +Рассмотрим алгоритм перемножения матриц с использованием трёх вложенных циклов. Сложность такого +алгоритма по определению должна составлять `O(n³)`, но есть особенности, связанные со средой +выполнения — скорость работы алгоритма зависит от последовательности, в которой выполняются циклы. + +Сравним различные варианты перестановок вложенных циклов и время выполнения алгоритмов. +Возьмём две матрицы: {`L×M`} и {`M×N`} → три цикла → шесть перестановок: +`LMN`, `LNM`, `MLN`, `MNL`, `NLM`, `NML`. + +Быстрее других отрабатывают те алгоритмы, которые пишут данные в результирующую матрицу *построчно слоями*: +`LMN` и `MLN`, — разница в процентах к другим алгоритмам значительная и зависит от среды выполнения. +{%- endcapture %} +{%- assign articles = articles | push: article_brief %} +{%- include main_page.html articles = articles -%} diff --git a/package.sh b/package.sh new file mode 100755 index 0000000..7fc78a1 --- /dev/null +++ b/package.sh @@ -0,0 +1,5 @@ +#!/bin/bash +echo "Подготовка архива для последующего развёртывания." +cd _site || exit +rm -rf ../pomodoro3.zip +7z a ../pomodoro3.zip ./* diff --git a/serve.sh b/serve.sh new file mode 100755 index 0000000..55b689c --- /dev/null +++ b/serve.sh @@ -0,0 +1,4 @@ +#!/bin/bash +echo "Локальное развёртывание для проверки корректности сборки." +jekyll serve --skip-initial-build --disable-disk-cache --host localhost +echo "Адрес сервера: http://localhost:4000"