diff --git a/07-matrix_experiment/cpp/Makefile b/07-matrix_experiment/cpp/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..ce3b51d23eaf59a70e34b7bcc6d3a783a9bcaa51 --- /dev/null +++ b/07-matrix_experiment/cpp/Makefile @@ -0,0 +1,26 @@ +.PHONY: test +test: matrix_experiment_sim matrix_experiment_real + @rm -rf out && mkdir out + @for exp in m1024-b16 m8192-b64 m65536-b256 m65536-b4096 ; do \ + for impl in smart naive ; do \ + echo "t-sim-$$exp-$$impl" ; \ + ./matrix_experiment_sim $$exp $$impl >out/t-sim-$$exp-$$impl ; \ + done ; \ + done + @for impl in smart naive ; do \ + echo "t-real-$$impl" ; \ + ./matrix_experiment_real $$impl >out/t-real-$$impl ; \ + done + +CXXFLAGS=-std=c++11 -O3 -Wall -Wextra -g -Wno-sign-compare + +matrix_experiment_sim: matrix_transpose.h matrix_tests.h matrix_experiment_sim.cpp + $(CXX) $(CPPFLAGS) $(CXXFLAGS) matrix_experiment_sim.cpp -o $@ + +matrix_experiment_real: matrix_transpose.h matrix_tests.h matrix_experiment_real.cpp + $(CXX) $(CPPFLAGS) $(CXXFLAGS) matrix_experiment_real.cpp -o $@ + +.PHONY: clean +clean:: + rm -f matrix_experiment_sim matrix_experiment_real + rm -rf out diff --git a/07-matrix_experiment/cpp/matrix_experiment_real.cpp b/07-matrix_experiment/cpp/matrix_experiment_real.cpp new file mode 100644 index 0000000000000000000000000000000000000000..f46c38acc3354119015eafa61ac47253027e5ffd --- /dev/null +++ b/07-matrix_experiment/cpp/matrix_experiment_real.cpp @@ -0,0 +1,90 @@ +#include <functional> +#include <string> +#include <vector> +#include <cstdio> +#include <cmath> +#include <iostream> + +#include <time.h> + +using namespace std; + +// If the condition is not true, report an error and halt. +#define EXPECT(condition, message) do { if (!(condition)) expect_failed(message); } while (0) + +void expect_failed(const string& message) { + cerr << "Test error: " << message << endl; + exit(1); +} + +class Matrix { + vector<unsigned> items; + unsigned &item(unsigned i, unsigned j) { return items[i*N + j]; } + public: + unsigned N; + Matrix(unsigned N) { this->N = N; items.resize(N*N, 0); } + + void swap(unsigned i1, unsigned j1, unsigned i2, unsigned j2) + { + // EXPECT(i1 < N && j1 < N && i2 < N && j2 < N, "Swap out of range: " + coord_string(i1, j1) + " with " + coord_string(i2, j2) + "."); + std::swap(item(i1, j1), item(i2, j2)); + } + + void naive_transpose() + { + for (unsigned i=0; i<N; i++) + for (unsigned j=0; j<i; j++) + swap(i, j, j, i); + } + +#include "matrix_transpose.h" +}; + +void real_test(bool naive) +{ + for (int e=40; e<=112; e++) { + unsigned N = (unsigned) pow(2, e/8.); + Matrix m(N); + + clock_t start_time, stop_time; + unsigned tries = 1; + do { + start_time = clock(); + for (unsigned t=0; t < tries; t++) { + if (naive) + m.naive_transpose(); + else + m.transpose(); + } + stop_time = clock(); + tries *= 2; + } while (stop_time - start_time < CLOCKS_PER_SEC/10); + // It is guaranteed that the total number of tries is odd :) + + double ns_per_item = (double)(stop_time - start_time) / CLOCKS_PER_SEC / (N*(N-1)) / tries * 1e9; + printf("%d\t%.6f\n", N, ns_per_item); + } +} + +int main(int argc, char **argv) +{ + if (argc != 2) { + fprintf(stderr, "Usage: %s (smart|naive)\n", argv[0]); + return 1; + } + + std::string mode = argv[1]; + + bool naive; + if (mode == "smart") + naive = false; + else if (mode == "naive") + naive = true; + else { + fprintf(stderr, "The argument must be either 'smart' or 'naive'\n"); + return 1; + } + + real_test(naive); + return 0; +} diff --git a/07-matrix_experiment/cpp/matrix_experiment_sim.cpp b/07-matrix_experiment/cpp/matrix_experiment_sim.cpp new file mode 100644 index 0000000000000000000000000000000000000000..316db527d089075e24dd4707e9f53078989a5a86 --- /dev/null +++ b/07-matrix_experiment/cpp/matrix_experiment_sim.cpp @@ -0,0 +1,80 @@ +#include <functional> +#include <string> +#include <vector> +#include <cstdio> +#include <cmath> +#include <string> +#include <iostream> + +#include <time.h> + +using namespace std; + +// If the condition is not true, report an error and halt. +#define EXPECT(condition, message) do { if (!(condition)) expect_failed(message); } while (0) + +void expect_failed(const string& message) { + cerr << "Test error: " << message << endl; + exit(1); +} + +#include "matrix_tests.h" + +void simulated_test(unsigned M, unsigned B, bool naive) +{ + for (int e=20; e<=52; e++) { + unsigned N = (unsigned) pow(2, e/4.); + TestMatrix m(N, M, B, 0); + m.fill_matrix(); + m.reset_stats(); + if (naive) + m.naive_transpose(); + else + m.transpose(); + + double misses_per_item = (double) m.stat_cache_misses / (N*(N-1)); + printf("%d\t%.6f\n", N, misses_per_item); + + m.check_result(); + } +} + +vector<pair<string, function<void(bool n)>>> tests = { +// M B + { "m1024-b16", [](bool n) { simulated_test( 1024, 16, n); } }, + { "m8192-b64", [](bool n) { simulated_test( 8192, 64, n); } }, + { "m65536-b256", [](bool n) { simulated_test(65536, 256, n); } }, + { "m65536-b4096", [](bool n) { simulated_test(65536, 4096, n); } }, +}; + +int main(int argc, char **argv) +{ + if (argc != 3) { + fprintf(stderr, "Usage: %s <test> (smart|naive)\n", argv[0]); + return 1; + } + + std::string which_test = argv[1]; + std::string mode = argv[2]; + + bool naive; + if (mode == "smart") + naive = false; + else if (mode == "naive") + naive = true; + else + { + fprintf(stderr, "Last argument must be either 'smart' or 'naive'\n"); + return 1; + } + + for (const auto& test : tests) { + if (test.first == which_test) { + test.second(naive); + return 0; + } + } + + fprintf(stderr, "Unknown test %s\n", which_test.c_str()); + return 1; +} diff --git a/07-matrix_experiment/cpp/matrix_tests.h b/07-matrix_experiment/cpp/matrix_tests.h new file mode 100644 index 0000000000000000000000000000000000000000..6110f8efc37782f04f949fb30f9e18392cc325e2 --- /dev/null +++ b/07-matrix_experiment/cpp/matrix_tests.h @@ -0,0 +1,213 @@ +#include <string> +#include <vector> +#include <iostream> +#include <algorithm> + +/* A matrix stored in a simulated cache */ + +class CachedMatrix { + unsigned B; // Block size + unsigned mem_blocks; // Memory size in blocks + unsigned cache_blocks; // Cache size in blocks + unsigned cache_used; // How many blocks of cache we already used + + // We store the matrix as a one-dimensional array + vector<unsigned> items; + unsigned pos(unsigned i, unsigned j) { return i*N + j; } + + /* + * For each memory block, we keep the following structure. + * If the block is currently cached, we set cached == true + * and lru_prev/next point to neighboring blocks in the cyclic LRU list. + * Otherwise, cached == false and the block is not in the LRU. + */ + + class MemoryBlock { + public: + unsigned lru_prev, lru_next; + bool cached; + MemoryBlock() + { + lru_prev = lru_next = 0; + cached = false; + } + }; + + vector<MemoryBlock> blocks; + + // One block at the end of "blocks" serves as a head of the LRU list. + unsigned lru_head; + + public: + // Number of rows and columns of the matrix + unsigned N; + + int debug_level; // Verbosity + + CachedMatrix(unsigned N, unsigned M, unsigned B, int debug_level=0) + { + EXPECT(N > 0, "CachedMatrix must be non-empty."); + EXPECT(B > 0, "Blocks must be non-empty."); + EXPECT(!(M % B), "Cache size must be divisible by block size."); + EXPECT(M >= 2*B, "Cache must have at least 2 blocks."); + + unsigned NN = N*N; + items.resize(NN, 0); + + this->N = N; + this->B = B; + this->debug_level = debug_level; + mem_blocks = (NN+B-1) / B; + cache_blocks = M / B; + cache_used = 0; + + // Initialize the LRU list + blocks.resize(mem_blocks + 1); + lru_head = mem_blocks; + blocks[lru_head].lru_prev = lru_head; + blocks[lru_head].lru_next = lru_head; + + if (debug_level > 0) + cout << "\tMemory: " << mem_blocks << " blocks of " << B << " items, " << cache_blocks << " cached\n"; + } + + // Read value at position (i,j), used only in testing code + unsigned read(unsigned i, unsigned j) + { + EXPECT(i < N && j < N, "Read out of range: " + coord_string(i, j) + "."); + unsigned addr = pos(i, j); + access(addr); + return items[addr]; + } + + // Write value at position (i,j), used only in testing code + void write(unsigned i, unsigned j, unsigned data) + { + EXPECT(i < N && j < N, "Write out of range: " + coord_string(i, j) + "."); + unsigned addr = pos(i, j); + access(addr); + items[addr] = data; + } + + // Swap items (i1,j1) and (i2,j2) + void swap(unsigned i1, unsigned j1, unsigned i2, unsigned j2) + { + EXPECT(i1 < N && j1 < N && i2 < N && j2 < N, "Swap out of range: " + coord_string(i1, j1) + " with " + coord_string(i2, j2) + "."); + if (debug_level > 1) + cout << "\tSwap " << coord_string(i1, j1) << " " << coord_string(i2, j2) << endl; + unsigned addr1 = pos(i1, j1), addr2 = pos(i2, j2); + access(addr1); + access(addr2); + std::swap(items[addr1], items[addr2]); + } + + unsigned stat_cache_misses; + unsigned stat_accesses; + + // Reset statistic counters. + void reset_stats() + { + stat_cache_misses = 0; + stat_accesses = 0; + } + + static string coord_string(unsigned i, unsigned j) + { + return "(" + to_string(i) + "," + to_string(j) + ")"; + } + +#include "matrix_transpose.h" + + private: + // Bring the given address to the cache. + void access(unsigned addr) + { + int i = addr / B; // Which block to bring + if (blocks[i].cached) { + lru_remove(i); + } else { + if (cache_used < cache_blocks) { + // We still have room in the cache. + cache_used++; + if (debug_level > 1) + cout << "\t\tLoading block " << i << endl; + } else { + // We need to evict the least-recently used block to make space. + unsigned replace = blocks[lru_head].lru_prev; + lru_remove(replace); + EXPECT(blocks[replace].cached, "Internal error: Buggy LRU list."); + blocks[replace].cached = false; + if (debug_level > 1) + cout << "\t\tLoading block " << i << ", replacing " << replace << endl; + } + blocks[i].cached = true; + stat_cache_misses++; + } + lru_add_after(i, lru_head); + stat_accesses++; + } + + // Remove block from the LRU list. + void lru_remove(unsigned i) + { + unsigned prev = blocks[i].lru_prev; + unsigned next = blocks[i].lru_next; + blocks[prev].lru_next = next; + blocks[next].lru_prev = prev; + } + + // Add block at the given position in the LRU list. + void lru_add_after(unsigned i, unsigned after) + { + unsigned next = blocks[after].lru_next; + blocks[next].lru_prev = i; + blocks[after].lru_next = i; + blocks[i].lru_next = next; + blocks[i].lru_prev = after; + } +}; + +/* A cached matrix extended by methods for testing */ + +class TestMatrix : public CachedMatrix { + public: + TestMatrix(unsigned N, unsigned M, unsigned B, int debug_level = 0) : CachedMatrix(N, M, B, debug_level) { } + + // Fill matrix with a testing pattern. + void fill_matrix() + { + if (debug_level > 1) + cout << "\tInitializing\n"; + for (unsigned i = 0; i < N; i++) + for (unsigned j = 0; j < N; j++) + write(i, j, i*N + j); + } + + // Check that the pattern corresponds to the properly transposed matrix. + void check_result() + { + if (debug_level > 1) + cout << "\tChecking\n"; + for (unsigned i = 0; i < N; i++) { + for (unsigned j = 0; j < N; j++) { + unsigned want = j*N + i; + unsigned found = read(i, j); + unsigned found_i = found / N; + unsigned found_j = found % N; + EXPECT(found == want, + "Mismatch at position " + coord_string(i, j) + + ": expected element from " + coord_string(j, i) + + ", found element from " + coord_string(found_i, found_j) + + "."); + } + } + } + + // Transpose the matrix naively. + void naive_transpose() + { + for (unsigned i=0; i<N; i++) + for (unsigned j=0; j<i; j++) + swap(i, j, j, i); + } +}; diff --git a/07-matrix_experiment/python/Makefile b/07-matrix_experiment/python/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..26bbb0173f30893c9a5bf546be30b8a580038fe1 --- /dev/null +++ b/07-matrix_experiment/python/Makefile @@ -0,0 +1,17 @@ +TESTS=m1024-b16 m8192-b64 m65536-b256 m65536-b4096 +TESTFILES=$(addprefix out/t-sim-,$(TESTS)) + +.PHONY: test +test: $(addsuffix -smart,$(TESTFILES)) $(addsuffix -naive,$(TESTFILES)) + +out/t-sim-%-naive: + @mkdir -p out + ./matrix_experiment_sim.py $* naive >$@ + +out/t-sim-%-smart: + @mkdir -p out + ./matrix_experiment_sim.py $* smart >$@ + +.PHONY: clean +clean:: + rm -rf out __pycache__ diff --git a/07-matrix_experiment/python/matrix_experiment_sim.py b/07-matrix_experiment/python/matrix_experiment_sim.py new file mode 100755 index 0000000000000000000000000000000000000000..5f326f193d349f11ccb73b77c984ea26a4dba597 --- /dev/null +++ b/07-matrix_experiment/python/matrix_experiment_sim.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +import sys + +from matrix_tests import TestMatrix + +def simulated_test(M, B, naive): + for e in range(10, 25): + N = int(2 ** (e/2)) + print(" ", N, M, B, file=sys.stderr) + m = TestMatrix(N, M, B, 0) + m.fill_matrix() + m.reset_stats() + if naive: + m.naive_transpose() + else: + m.transpose() + misses_per_item = m.stat_cache_misses / (N*(N-1)) + print(N, misses_per_item, flush=True) + m.check_result() + +tests = { +# M B + "m1024-b16": lambda n: simulated_test( 1024, 16, n), + "m8192-b64": lambda n: simulated_test( 8192, 64, n), + "m65536-b256": lambda n: simulated_test(65536, 256, n), + "m65536-b4096": lambda n: simulated_test(65536, 4096, n), +} + +if len(sys.argv) == 3: + test = sys.argv[1] + if sys.argv[2] == "smart": + naive = False + elif sys.argv[2] == "naive": + naive = True + else: + raise ValueError("Last argument must be either 'smart' or 'naive'") + if test in tests: + tests[test](naive) + else: + raise ValueError("Unknown test {}".format(test)) +else: + raise ValueError("Usage: {} <test> (smart|naive)".format(sys.argv[0])) diff --git a/07-matrix_experiment/python/matrix_tests.py b/07-matrix_experiment/python/matrix_tests.py new file mode 100644 index 0000000000000000000000000000000000000000..f002701a4e68f65cbde033b2058a79d71aad61ba --- /dev/null +++ b/07-matrix_experiment/python/matrix_tests.py @@ -0,0 +1,165 @@ +from matrix_transpose import Matrix +import numpy + +# Description of memory blocks is stored in an array blocks[block_index][B_xxx]. +# If the block is cached, B_CACHED is 1 and B_LRU_NEXT and B_LRU_PREV point to +# neighboring blocks in the cyclic LRU list. Otherwise, B_CACHED is 0 and the block +# is not in the LRU. +B_CACHED = 0 +B_LRU_NEXT = 1 +B_LRU_PREV = 2 + +class CachedMatrix(Matrix): + """A matrix stored in simulated cache""" + + def __init__(self, N, M, B, debug_level=0): + assert N>0, "CachedMatrix must be non-empty." + assert B>0, "Blocks must be non-empty." + assert M%B == 0, "Cache size must be divisible by block size." + assert M >= 2*B, "Cache must have at least 2 blocks." + + Matrix.__init__(self, N) + + NN = N*N + self.items = numpy.zeros(shape=NN, dtype=numpy.int32, order="C") + + self.B = B + self.M = M + self.debug_level = debug_level + self.mem_blocks = (NN+B-1) // B + self.cache_blocks = M//B + self.cache_used = 0 + + # Initialize the LRU list. There is a virtual block right after the last real block, + # which serves as a head of the cyclic LRU list. + self.blocks = numpy.zeros(shape=(self.mem_blocks+1, 3), dtype=numpy.int32, order="C") + self.lru_head = self.mem_blocks + self.blocks[self.lru_head, B_LRU_NEXT] = self.lru_head + self.blocks[self.lru_head, B_LRU_PREV] = self.lru_head + + self.reset_stats() + + if debug_level > 0: + print("\tMemory: {} blocks of {} items, {} cached".format(self.mem_blocks, self.B, self.cache_blocks)) + + def _pos(self, i, j): + """Convert position in matrix to linear address in simulated memory.""" + + return i*self.N + j + + def read(self, i, j): + """Read value at position (i,j), used only in testing code.""" + + assert i >= 0 and i < self.N and j >= 0 and j < self.N, "Read out of range: ({},{})".format(i, j) + addr = self._pos(i, j) + self._access(addr) + return self.items[addr] + + def write(self, i, j, value): + """Write value at position (i,j), used only in testing code.""" + + assert i >= 0 and i < self.N and j >= 0 and j < self.N, "Write out of range: ({},{})".format(i, j) + addr = self._pos(i, j) + self._access(addr) + self.items[addr] = value + + def swap(self, i1, j1, i2, j2): + """Swap items (i1,j1) and (i2,j2).""" + + assert i1 >= 0 and i1 < self.N and j1 >= 0 and j1 < self.N and \ + i2 >= 0 and i2 < self.N and j2 >= 0 and j2 < self.N, \ + "Swap out of range: ({},{}) with ({},{})".format(i1, j1, i2, j2) + if self.debug_level > 1: + print("\tSwap ({},{}) ({},{})".format(i1, j1, i2, j2)) + addr1 = self._pos(i1, j1) + addr2 = self._pos(i2, j2) + self._access(addr1) + self._access(addr2) + items = self.items + items[addr1], items[addr2] = items[addr2], items[addr1] + + def reset_stats(self): + """Reset statistic counters.""" + + self.stat_cache_misses = 0 + self.stat_accesses = 0 + + def _access(self, addr): + """Bring the given address to the cache.""" + + blocks = self.blocks + i = addr // self.B # Which block to bring + if blocks[i, B_CACHED] > 0: + self._lru_remove(i) + else: + if self.cache_used < self.cache_blocks: + # We still have room in the cache. + self.cache_used += 1 + if self.debug_level > 1: + print("\t\tLoading block {}".format(i)) + else: + # We need to evict the least-recently used block to make space. + replace = blocks[self.lru_head, B_LRU_PREV] + self._lru_remove(replace) + assert blocks[replace, B_CACHED] > 0, "Internal error: Buggy LRU list" + blocks[replace, B_CACHED] = 0 + if self.debug_level > 1: + print("\t\tLoading block {}, replacing {}".format(i, replace)) + blocks[i, B_CACHED] = 1 + self.stat_cache_misses += 1 + self._lru_add_after(i, self.lru_head) + self.stat_accesses += 1 + + def _lru_remove(self, i): + """Remove block from the LRU list.""" + + blocks = self.blocks + prev, next = blocks[i, B_LRU_PREV], blocks[i, B_LRU_NEXT] + blocks[prev, B_LRU_NEXT] = next + blocks[next, B_LRU_PREV] = prev + + def _lru_add_after(self, i, after): + """Add block at the given position in the LRU list.""" + + blocks = self.blocks + next = blocks[after, B_LRU_NEXT] + blocks[after, B_LRU_NEXT] = i + blocks[next, B_LRU_PREV] = i + blocks[i, B_LRU_NEXT] = next + blocks[i, B_LRU_PREV] = after + +class TestMatrix(CachedMatrix): + """A cached matrix extended by methods for testing.""" + + # Constructor is inherited + + def fill_matrix(self): + """Fill matrix with a testing pattern.""" + + if self.debug_level > 1: + print("\tInitializing") + N = self.N + for i in range(N): + for j in range(N): + self.write(i, j, i*N + j) + + def check_result(self): + """Check that the pattern corresponds to the properly transposed matrix.""" + + if self.debug_level > 1: + print("\tChecking") + N = self.N + for i in range(N): + for j in range(N): + want = j*N + i + have = self.read(i, j) + have_i = have // N + have_j = have % N + assert have == want, "Mismatch at position ({},{}): expected element from ({},{}), found element from ({},{})".format(i, j, j, i, have_i, have_j) + + def naive_transpose(self): + """Transpose the matrix naively.""" + + for i in range(self.N): + for j in range(i): + self.swap(i, j, j, i) diff --git a/07-matrix_experiment/task.md b/07-matrix_experiment/task.md new file mode 100644 index 0000000000000000000000000000000000000000..23681110d99d62a1ee34aeb378689bc673890210 --- /dev/null +++ b/07-matrix_experiment/task.md @@ -0,0 +1,90 @@ +## Goal + +The goal of this assignment is to evaluate your implementation of cache-oblivious +matrix transposition experimentally and to compare it with the trivial algorithm +which transposes by definition. + +You are given a test program (`matrix_experiment_sim`) which evaluates your +implementation from the previous assignment on different simulated caches and +matrices of different sizes. For each experiment, the average number of cache +misses per item is reported (the diagonal items which do not move are not +counted). The program also evaluates performance of the trivial transposition algorithm. +The simulated cache is fully associative and uses LRU replacement strategy. + +You should run these experiments and write a report, which contains one plot of +the measured data for each cache type, showing dependency of the average number of +misses per item on the matrix size. There should be two curves in each plot: one for your +algorithm, another for the trivial one. + +The report should discuss the experimental results and try to explain the observed +behavior (including any strange anomalies) using theory from the lectures. +If you want, you can carry out further experiments to gain better understanding +of the algorithm and include these in the report. + +You should submit a PDF file with the report (and no source code). +You will get 1 temporary point upon submission if the file is syntactically correct; +proper points will be assigned later. + +## Test program + +The test program is given two arguments: +- Cache type: + - `m1024-b16` – cache of 1024 items organized in 16-item blocks + - `m8192-b64` – cache of 8192 items organized in 64-item blocks + - `m65536-b256` – cache of 65536 items organized on 256-item blocks + - `m65536-b4096` – cache of 65536 items organized in 4096-item blocks +- The implementation to test (`smart` or `naive`). + +The output of the program contains one line per experiment, which consists of +the matrix size and the average number of cache misses per item. + +*Warning:* The Python tests are slow, even though they use only a subset of the +matrix sizes. They can take about one hour to complete. +If your machine has multiple processors or cores, you can try `make -j` +to run the tests in parallel. + +## Optional: Tests on real hardware (for 5 extra points) + +You can also test your transposition algorithm on real hardware +using the `matrix_experiment_real` program. The matrix is stored in row-major +order, each item takes 4 bytes. + +The program takes one parameter, the implementation to test: `smart` or `naive`. +Its output contains one line per experiment, which consists of the matrix size +and the average time per item in nanoseconds. + +However, the program is available only for C++, because general slowness of +Python completely hides all cache effects. + +Again, your report should show a plot of the measured data and discuss the observed +effects. You should also include the configuration of caches in your machine. +(If you are using Linux, you can try the `machinfo` script from +[this repository](https://gitlab.kam.mff.cuni.cz/mj/aim.git).) + +## Hints + +The following tools can be useful for producing nice plots: +- [pandas](https://pandas.pydata.org/) +- [matplotlib](https://matplotlib.org/) +- [gnuplot](http://www.gnuplot.info/) + +A quick checklist for plots: +- Is there a caption explaining what is plotted? +- Are the axes clearly labelled? Do they have value ranges and units? +- Have you mentioned that this axis has logarithmic scale? (Logarithmic graphs + are more fitting in some cases, but you should tell.) +- Is it clear which curve means what? +- Is it clear what are the measured points and what is an interpolated + curve between them? +- Are there any overlaps? (E.g., the most interesting part of the curve + hidden underneath a label?) +- **Is the graph distorted by compression artifacts?** (No, you shouldn't use JPEG for plots!) + +In your discussion, please distinguish the following kinds of claims. +It should be always clear which is which: +- Experimental results (i.e., the raw data you obtained from the experiments) +- Theoretical facts (i.e., claims we have proved mathematically) +- Your hypotheses (e.g., when you claim that the graph looks like something is true, + but you are not able to prove rigorously that it always holds) + +Source code templates can be found in [git](https://gitlab.kam.mff.cuni.cz/datovky/assignments/-/tree/master).