Lista Programação Concorrente e distribuída
+Resolução feita por Samuel Cavalcanti
+Copyright © 2022 Samuel Cavalcanti
+ +diff --git a/mkdocs_with_pdf/drivers/web_driver.py b/mkdocs_with_pdf/drivers/web_driver.py new file mode 100644 index 00000000..e7c53224 --- /dev/null +++ b/mkdocs_with_pdf/drivers/web_driver.py @@ -0,0 +1,11 @@ +from typing import Protocol + + +class WebDriver(Protocol): + + def render(self, html: str) -> str: + """ + Receive an html page in string format and execute the javascript + returning the new rendered html + """ + ... \ No newline at end of file diff --git a/tests/assets/input.html b/tests/assets/input.html new file mode 100644 index 00000000..71dae501 --- /dev/null +++ b/tests/assets/input.html @@ -0,0 +1,2630 @@ +
+ + + + +Copyright © 2022 Samuel Cavalcanti
+ +Capítulo 1: 1-6, 9; (7 questões)
+Devise formulas for the functions that calculate my_first_i and my_last_i in the global sum example. Remember that each core should be assigned roughly the same number of elements of computations in the loop. Hint: First consider the case when n is evenly divisible by p
+ +We’ve implicitly assumed that each call to Compute_next_value requires roughly the same amount of work as the other calls. How would you change your answer to the preceding question if call i = k requires k + 1 times as much work as the call with i = 0? So if the first call (i = 0) requires 2 milliseconds, the second call (i = 1) requires 4, the third (i = 2) requires 6, and so on.
+ +Try to write pseudo-code for the tree-structured global sum illustrated in +Figure 1.1. Assume the number of cores is a power of two (1, 2, 4, 8, . . . ).
+ +As an alternative to the approach outlined in the preceding problem we can use C’s bitwise operators to implement the tree-structured global sum. In order to see how this works, it helps to write down the binary (base 2) representation of each of the core ranks, and note the pairings during each stage
+ +What happens if your pseudo-code in Exercise 1.3 or Exercise 1.4 is run when the number of cores is not a power of two (e.g., 3, 5, 6, 7) ? Can you modify the +pseudo-code so that it will work correctly regardless of the number of cores ?
+ +Derive formulas for the number of receives and additions that core 0 carries out using:
+ a. the original pseudo-code for a global sum
+ b. the tree-structured global sum.
+Make a table showing the numbers of receives and additions carried out by core
+0 when the two sums are used with 2, 4, 8, . . . , 1024 cores.
Write an essay describing a research problem in your major that would benefit from the use of parallel computing. Provide a rough outline of how parallelism would be used. Would you use task- or data-parallelism ?
+ +Devise formulas for the functions that calculate my_first_i and my_last_i in the global sum example. Remember that each core should be assigned roughly the same number of elements of computations in the loop. Hint: First consider the case when n is evenly divisible by p
+struct range
+{
+ int first;
+ int last;
+};
+struct range new_range(int thread_index, int p, int n)
+{
+ struct range r;
+
+ int division = n / p;
+ int rest = n % p;
+
+ if (rest == 0)
+ {
+ r.first = thread_index * division;
+ r.last = (thread_index + 1) * division;
+ }
+ else
+ {
+ r.first = thread_index == 0 ? 0 : thread_index * division + rest;
+ r.last = (thread_index + 1) * division + rest;
+ }
+
+ if (r.last > n)
+ r.last = n;
+
+ return r;
+}
+
+struct range new_range_2(int thread_index, int p, int n)
+{
+ struct range r;
+
+ int division = n / p;
+ int rest = n % p;
+
+ if (thread_index < rest)
+ {
+ r.first = thread_index * (division + 1);
+ r.last = r.first + division + 1;
+ }
+ else
+ {
+ r.first = thread_index * division + rest;
+ r.last = r.first + division;
+ }
+
+ return r;
+}
+
First 0 Last 20 m Last - First: 20
+First 20 Last 40 m Last - First: 20
+First 40 Last 60 m Last - First: 20
+First 60 Last 80 m Last - First: 20
+First 80 Last 100 m Last - First: 20
+First 0 Last 25 m Last - First: 25
+First 25 Last 50 m Last - First: 25
+First 50 Last 75 m Last - First: 25
+First 75 Last 99 m Last - First: 24
+First 99 Last 123 m Last - First: 24
+Test question 1 success
+OLD new range
+First 0 Last 27 m Last - First: 27
+First 27 Last 51 m Last - First: 24
+First 51 Last 75 m Last - First: 24
+First 75 Last 99 m Last - First: 24
+First 99 Last 123 m Last - First: 24
+
We’ve implicitly assumed that each call to Compute_next_value requires roughly the same amount of work as the other calls. How would you change your answer to the preceding question if call i = k requires k + 1 times as much work as the call with i = 0? So if the first call (i = 0) requires 2 milliseconds, the second call (i = 1) requires 4, the third (i = 2) requires 6, and so on.
+Exemplo, Supondo que k = 10, temos o seguinte vetor de índice:
+indices = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
+utilizando a lógica da somatório de gauss e organizando os indicies temos um array normalizado:
+normalized_array = [ [0, 9], [1, 8], [2, 7], [3, 6], [4, 5] ]
+onde o custo de cada índice do normalized_array é igual, por tanto
+podemos usar o algoritmo da questão 1 aplicado ao normalized_array
+resultando:
Thread | +normalized_array | +
---|---|
1 | +0,1 | +
2 | +2,3 | +
3 | +4 | +
Thread | +Compute_next_value | +cost | +
---|---|---|
1 | +0, 9,1, 8 | +44 | +
2 | +2, 7,3, 6 | +44 | +
3 | +4, 5 | +22 | +
Try to write pseudo-code for the tree-structured global sum illustrated in +Figure 1.1. Assume the number of cores is a power of two (1, 2, 4, 8, . . . ).
+Para criar a árvore, foi considerado que o vetor principal já foi igualmente espaçado entre as p threads, usando o algoritmo da questão 1.
+Neste caso foi representado o nó, como uma estrutura que possui um vetor de vizinhos e outro ponteiro para um vetor de inteiros, na prática, +o ponteiro para um vetor de inteiros, seria usando o design pattern chamado Future, ou um Option\<Future>.
+Também foi criado dois construtores um construtor que representa, +a inicialização do Nó por meio dos seus vizinhos a esquerda e direita, +usando na criação de nós intermediários da árvore, e a inicialização +do Nó por meio do seu dado, os nós inicializados por meio dos dados +abstrai os núcleos ou as threads que estão sendo usadas para +executar o algoritmo de alto custo computacional.
+class Node
+{
+public:
+ std::vector<Node *> neighborhoods;
+ std::vector<int> *data;
+ Node(Node *left, Node *right)
+ {
+ this->neighborhoods.push_back(left);
+ this->neighborhoods.push_back(right);
+ this->data = nullptr;
+ }
+ Node(std::vector<int> *data)
+ {
+ this->data = data;
+ }
+ ~Node()
+ {
+ delete this->data;
+ }
+};
+
Para criar a Árvore foi feita uma função recursiva que +a partir do nível mais baixo da árvore cria a raiz, ou seja, +a partir um vetor com p Nós ,a função vai sendo chamada recursivamente, +onde a cada chamada vai-se criando um nível acima da árvore, até +que se atinja a raiz, onde a cada nível o número de nós é dividido +por 2. Caso o número de nós inicial não for divisível pro 2, o algoritmo não funciona
+std::vector<Node *> create_tree_from_core_nodes(std::vector<Node *> nodes)
+{
+ auto size = nodes.size();
+
+ if (size / 2 == 1)
+ {
+ auto left = nodes[0];
+ auto right = nodes[1];
+ receive_value(left, right); // Left receive value from right
+ return {left};
+ }
+
+ auto new_nodes = std::vector<Node *>{};
+
+ for (auto i = 0; i < size; i += 2)
+ {
+ auto left = nodes[i];
+ auto right = nodes[i + 1];
+ receive_value(left, right); // Left receive value from right
+ new_nodes.push_back(left);
+ }
+
+ return create_tree_from_core_nodes(new_nodes);
+}
+
+Node *create_new_tree(std::vector<Node *> nodes)
+{
+ return create_tree_from_core_nodes(nodes)[0];
+}
+
Após criar a árvore basta percorrer-lá recursivamente, lembrando que na prática +compute_data, seria um join, ou um await, de uma thread.
+int compute_data(std::vector<int> *data)
+{
+ auto total = 0;
+ auto size = data->size();
+ for (auto i = 0; i < size; i++)
+ {
+ total += data->at(i);
+ }
+
+ return total;
+}
+int compute_node(Node &node)
+{
+
+ int result_data = node.data == nullptr ? 0 : compute_data(node.data);
+
+ for (auto neighborhood : node.neighborhoods)
+ result_data += compute_node(*neighborhood);
+
+ return result_data;
+}
+
As an alternative to the approach outlined in the preceding problem we can use C’s bitwise operators to implement the tree-structured global sum. In order to see how this works, it helps to write down the binary (base 2) representation of each of the core ranks, and note the pairings during each stage
+Semelhante ao questão 3 sendo a diferença utilizar o bitwise << para dividir +o tamanho atual da função recursiva:
+std::vector<Node *> create_new_tree_bitwise(std::vector<Node *> nodes)
+{
+
+ auto size = nodes.size();
+
+ if (size >> 1 == 1) // alteração.
+ {
+ auto left = nodes[0];
+ auto right = nodes[1];
+ receive_value(left, right);
+ return {left};
+ }
+
+ auto new_nodes = std::vector<Node *>{};
+
+ for (auto i = 0; i < size; i += 2)
+ {
+ auto left = nodes[i];
+ auto right = nodes[i + 1];
+ receive_value(left, right); // Left receive value from right
+ new_nodes.push_back(left);
+ }
+
+ return create_new_tree_bitwise(new_nodes);
+}
+
What happens if your pseudo-code in Exercise 1.3 or Exercise 1.4 is run when the number of cores is not a power of two (e.g., 3, 5, 6, 7) ? Can you modify the +pseudo-code so that it will work correctly regardless of the number of cores ?
+Se por exemplo, o número de cores, ou Nós for 3 por exemplo, existirá nós que serão "esquecidos" no algoritmo, por tanto +o algoritmo não funcionará corretamente. +
auto size = nodes.size();// size = 3
+
+if (size >> 1 == 1) // alteração.
+{
+ auto left = nodes[0];
+ auto right = nodes[1];
+ // node[2] foi esquecido
+ receive_value(left, right); // Left receive value from right
+ return {left};
+}
+
ou se o por exemplo o número de nós for 7, a última iteração do laço for, os indices i, i+1, serão respectivamente +6 e 7, ou seja será acessado um endereço inválido 7, uma vez que os indices vão de 0 até 6. +
auto new_nodes = std::vector<Node *>{};
+
+ for (auto i = 0; i < size; i += 2)
+ {
+ auto left = nodes[i];
+ auto right = nodes[i + 1];
+ receive_value(left, right); // Left receive value from right
+ new_nodes.push_back(left);
+ }
+
+ return create_new_tree_bitwise(new_nodes);
+
Para isso foram feitas as seguintes modificações:
+ - adicionado condicionamento para verificar se o tamanho é igual 3
+ - alterado o bitwise para apenas um comparador size ==2
+ - verificado se o tamanho dos nós é par, caso nãos seja adicionado uma logica extra.
std::vector<Node *> create_new_tree_bitwise(std::vector<Node *> nodes)
+{
+
+ auto size = nodes.size();
+
+ if (size == 2)
+ {
+ auto left = nodes[0];
+ auto right = nodes[1];
+ receive_value(left, right);
+ return {left}; // Construtor C++ Moderno.
+ }
+ if (size == 3)
+ {
+ auto left = nodes[0];
+ auto middle = nodes[1];
+ auto right = nodes[2];
+ receive_value(left, middle); // Left receive value from middle
+ receive_value(left, right); // Left receive value from right
+ return {left}; // Construtor C++ Moderno.
+ }
+
+ auto new_nodes = std::vector<Node *>{};
+
+ if (size % 2 != 0) // lógica extra.
+ {
+ size = size - 1;
+ new_nodes.push_back(nodes[size]);
+ }
+
+ for (auto i = 0; i < size; i += 2)
+ {
+ auto left = nodes[i];
+ auto right = nodes[i + 1];
+ receive_value(left, right); // Left receive value from right
+ new_nodes.push_back(left);
+ }
+
+ return create_new_tree_bitwise(new_nodes);
+}
+
Além de adicionar uma verificação para saber se o tamanho é par, +foi adicionado dois comandos extras, o primeiro é alterar o tamanho (size), para um valor menor, uma vez que estávamos acessando um índice +maior que o permitido. Segundo foi adicionar o nó que não será percorrido +pelo laço para o vetor new_nodes que será a entrada da próxima função recursiva
+ if (size % 2 != 0) // verificação se é par.
+ {
+ size = size - 1; //1
+ new_nodes.push_back(nodes[size]); // 2
+ }
+
Percebemos que além do 2/2 == 1, a divisão inteira de 3/2 também é igual 1. Por tanto além do caso base de quando o tamanho do vetor de nós ser igual a 2, temos que tratar também quando o número de nós ser igual a 3.
+ if (size == 2)
+ {
+ auto left = nodes[0];
+ auto right = nodes[1];
+ receive_value(left, right);
+ return {left}; // Construtor C++ Moderno.
+ }
+ if (size == 3)
+ {
+ auto left = nodes[0];
+ auto middle = nodes[1];
+ auto right = nodes[2];
+ receive_value(left, middle); // Left receive value from middle
+ receive_value(left, right); // Left receive value from right
+ return {left}; // Construtor C++ Moderno.
+ }
+
Como no exemplo abaixo, onde a segunda iteração do algoritmo o número de nós é 3.
+ +Derive formulas for the number of receives and additions that core 0 carries out using:
+ a. the original pseudo-code for a global sum
+ b. the tree-structured global sum.
+Make a table showing the numbers of receives and additions carried out by core
+0 when the two sums are used with 2, 4, 8, . . . , 1024 cores.
Cores | +Naive | +Tree | +
---|---|---|
2 | +1 | +1 | +
4 | +3 | +2 | +
8 | +7 | +3 | +
16 | +15 | +4 | +
32 | +31 | +5 | +
64 | +63 | +6 | +
128 | +127 | +7 | +
256 | +255 | +8 | +
512 | +512 | +9 | +
1024 | +1023 | +10 | +
Podemos observar claramente que a abordagem ingênua segue a formula, +p -1 e quando usamos a árvore, percebemos que a cada 2 núcleos, +o número de ligações amentar em 1, ou seja, log(p) de base 2. +Podemos ver o número de ligações crescendo linearmente com cada dois núcleos na imagem abaixo +
+Write an essay describing a research problem in your major that would benefit from the use of parallel computing. Provide a rough outline of how parallelism would be used. Would you use task- or data-parallelism ?
+Em contexto de aplicações web, especificamente a camada front-end, vem se popularizando frameworks que utilizam o WebAssembly (Wasm). O WebAssembly é um novo tipo de código que pode ser executado em browsers modernos — se trata de uma linguagem de baixo nível como assembly, com um formato binário compacto que executa com performance quase nativa e que fornece um novo alvo de compilação para linguagens como C/C++, para que possam ser executadas na web developer.mozilla.org. Através do Wasm é possível +implementar algoritmos de Tiny machine learning (TinyML) para classificação, analise de dados sem a necessidade de comunicação com backend. TinyML é amplamente definido como uma rápida e crescente área de aprendizado de máquina e suas aplicações que inclui hardware, algoritmos e software capazes de performar analise de dados em dispositivos de baixo consumo de energia, tipicamente em milliwatts, assim habilitado variados casos de uso e dispositivos que possuem bateria. Em um navegador, ou durante a navegação o usuário está a +todo momento produzindo dados que muitas vezes estão sendo enviados +de forma bruta ou quase bruta para a camada de aplicação ou back-end, onde nela é gasto processamento e memória para a primeira etapa de classificação ou analise dos dados. Uma vez analisado desempenho de técnicas e algoritmos de TinyML utilizando WebAssembly, pode ser possível transferir a responsabilidade da analise dos dados para o front-end. Em contexto de navegador quase tudo é paralelo ou distribuído, +uma aba ou tab em inglês é um processo diferente. Criar um uma extensão, que faça aquisição e analise dos dados de diferentes abas, seria criar um sistema que se comunica com diferentes processos por meio de mensagens e o algoritmo de aprendizado pode fazer uso de idealmente uma ou duas threads para realizar a analise rapidamente. Por tanto é um +sistema que é task-and-data parallel.
+Capítulo 2: 1-3, 5, 7, 10, 15-17, 19-21, 24; (13 questões)
+When we were discussing floating point addition, we made the simplifying assumption that each of the functional units took the same amount of time. Suppose that fetch and store each take 2 nanoseconds and the remaining operations each take 1 nanosecond.
+How long does a floating point addition take with these assumptions ?
+How long will an unpipelined addition of 1000 pairs of floats take with these assumptions ?
+How long will a pipelined addition of 1000 pairs of floats take with these assumptions ?
+The time required for fetch and store may vary considerably if the operands/results are stored in different levels of the memory hierarchy. Suppose that a fetch from a level 1 cache takes two nanoseconds, while a fetch from a level 2 cache takes five nanoseconds, and a fetch from main memory takes fifty nanoseconds. What happens to the pipeline when there is a level 1 cache miss on a fetch of one of the operands? What happens when there is a level 2 miss ?
+ +Explain how a queue, implemented in hardware in the CPU, could be used to improve the performance of a write-through cache.
+ +Recall the example involving cache reads of a two-dimensional array (page 22). How does a larger matrix and a larger cache affect the performance of the two pairs of nested loops? What happens if MAX = 8 and the cache can store four lines ? How many misses occur in the reads of A in the first pair of nested loops ? How many misses occur in the second pair ?
+ +Does the addition of cache and virtual memory to a von Neumann system change its designation as an SISD system ? What about the addition of +pipelining? Multiple issue? Hardware multithreading ?
+ +Discuss the differences in how a GPU and a vector processor might execute the following code:
+
Suppose a program must execute 10¹² instructions in order to solve a particular problem. Suppose further that a single processor system can solve the problem in 10⁶ seconds (about 11.6 days). So, on average, the single processor system executes 10⁶ or a million instructions per second. Now suppose that the program has been parallelized for execution on a distributed-memory system. Suppose also that if the parallel program uses p processors, each processor will execute 10¹² /p instructions and each processor must send 10⁹ ( p − 1) messages. Finally, suppose that there is no additional overhead in executing the +parallel program. That is, the program will complete after each processor has executed all of its instructions and sent all of its messages, and there won’t be any delays due to things such as waiting for messages.
+Suppose it takes 10⁻⁹ seconds to send a message. How long will it take the program to run with 1000 processors, if each processor is as fast as the single processor on which the serial program was run ?
+Suppose it takes 10⁻³ seconds to send a message. How long will it take the program to run with 1000 processors ? + Resposta questão 10
+Suppose a shared-memory system uses snooping cache coherence and +write-back caches. Also suppose that core 0 has the variable x in its cache, and it executes the assignment x = 5. Finally suppose that core 1 doesn’t have x in its cache, and after core 0’s update to x, core 1 tries to execute y = x. What value will be assigned to y ? Why ?
+Suppose that the shared-memory system in the previous part uses a +directory-based protocol. What value will be assigned to y ? Why ?
+A parallel program that obtains a speedup greater than p—the number of processes or threads—is sometimes said to have superlinear speedup. However, many authors don’t count programs that overcome “resource limitations” as having superlinear speedup. For example, a program that must use secondary storage for its data when it’s run on a single processor system might be able to fit all its data into main memory when run on a large distributed-memory system. Give another example of how a program might overcome a resource limitation and obtain speedups greater than p
+ +Suppose \(T_{serial} = n\) and \(T_{parallel} = \frac{n}{p} + log_2 (p)\), where times are in microseconds. If we increase \(p\) by a factor of \(k\), find a formula for how much we’ll need to increase n in order to maintain constant efficiency. How much should we increase \(n\) by if we double the number of processes from 8 to 16 ? Is the parallel program scalable ?
+ +Is a program that obtains linear speedup strongly scalable ? Explain your answer.
+ +Bob has a program that he wants to time with two sets of data, input_data1 and input_data2. To get some idea of what to expect before adding timing functions to the code he’s interested in, he runs the program with two sets of data and the Unix shell command time: +
+ The timer function Bob is using has millisecond resolution. Should Bob use it to time his program with the first set of data ? What about the second set of data ? Why or why not ? + +When we were discussing floating point addition, we made the simplifying assumption that each of the functional units took the same amount of time. Suppose that fetch and store each take 2 nanoseconds and the remaining operations each take 1 nanosecond.
+ a. How long does a floating point addition take with these assumptions ?
b. How long will an unpipelined addition of 1000 pairs of floats take with these assumptions ?
+c. How long will a pipelined addition of 1000 pairs of floats take with these assumptions ?
+d. The time required for fetch and store may vary considerably if the operands/results are stored in different levels of the memory hierarchy. Suppose that a fetch from a level 1 cache takes two nanoseconds, while a fetch from a level 2 cache takes five nanoseconds, and a fetch from main memory takes fifty nanoseconds. What happens to the pipeline when there is a level 1 cache miss on a fetch of one of the operands? What happens when there is a level 2 miss ?
+Instructions | +Time in nanosecond | +
---|---|
Fetch | +2 | +
Store | +2 | +
Functional OP | +1 | +
"As an alternative, suppose we divide our floating point adder into seven separate pieces of hardware or functional units. The first unit will fetch two operands, +the second will compare exponents, and so on." (Página 26)
+O Author do livro considera que existe sete operações, considerando que duas delas são fetch e store custa 2 nanosegundos e o restante 1 nanosegundo.
+ 1*5 +2*2 = 9 nanosegundos
+
Considerando que exitem 1000 pares de valores vão serem somados:
+ 1000*9 = 9000 nanosegundos
+
foi pensado o seguinte: Nó memento que o dado passa pelo fetch, e vai para a próxima operação já +é realizado o fetch da segunda operação. Executando o pipeline:
+Tempo em nanosegundos | +Fetch | +OP1 | +OP2 | +OP3 | +OP4 | +OP5 | +Store | +
---|---|---|---|---|---|---|---|
0 | +1 | +wait | +wait | +wait | +wait | +wait | +wait | +
2 | +2 | +1 | +wait | +wait | +wait | +wait | +wait | +
3 | +2 | +wait | +1 | +wait | +wait | +wait | +wait | +
4 | +3 | +2 | +wait | +1 | +wait | +wait | +wait | +
5 | +3 | +wait | +2 | +wait | +1 | +wait | +wait | +
6 | +4 | +3 | +wait | +2 | +wait | +1 | +wait | +
7 | +4 | +wait | +3 | +wait | +2 | +wait | +1 | +
8 | +5 | +4 | +wait | +3 | +wait | +2 | +wait | +
9 | +5 | +wait | +4 | +wait | +3 | +wait | +2 | +
10 | +6 | +5 | +wait | +4 | +wait | +3 | +2 | +
11 | +6 | +wait | +5 | +wait | +4 | +wait | +3 | +
Percebe-se que a primeira instrução irá ser finalizada ou sumir na tabela quanto for 9 segundos +ou seja,a primeira instrução dura 9 segundos, no entanto, no momento em que a primeira instrução +é finalizada,a segunda já começa a ser finalizada ou seja, demora apenas 2 nanosegundos até segunda operação ser finalizada e mais 2 nanosegundos para a terceira ser finalizada e assim por diante. Por tanto para executar todos os 1000 dados, o custo total fica:
+ 9 + 999*2 = 2007
+
No caso, considerando que a cache nível não falhe a tabela continua mesma, +pois o fetch e store custam o mesmo 2 nanosegundos:
+Tempo em nanosegundos | +Fetch | +OP1 | +OP2 | +OP3 | +OP4 | +OP5 | +Store | +
---|---|---|---|---|---|---|---|
10 | +6 | +5 | +wait | +4 | +wait | +3 | +2 | +
11 | +6 | +wait | +5 | +wait | +4 | +wait | +3 | +
mas se imaginarmos que na 12 iteração o Fetch e Store passa a custar 5 nanosegundos:
+Tempo em nanosegundos | +Fetch | +OP1 | +OP2 | +OP3 | +OP4 | +OP5 | +Store | +
---|---|---|---|---|---|---|---|
10 | +6 | +5 | +wait | +4 | +wait | +3 | +2 | +
11 | +6 | +wait | +5 | +wait | +4 | +wait | +3 | +
12 | +6 | +wait | +wait | +5 | +wait | +4 | +3 | +
13 | +6 | +wait | +wait | +wait | +5 | +4 | +3 | +
14 | +6 | +wait | +wait | +wait | +5 | +4 | +3 | +
15 | +7 | +6 | +wait | +wait | +5 | +4 | +3 | +
16 | +7 | +wait | +6 | +wait | +wait | +5 | +4 | +
Quando mais lento fica a transferência para a memória principal, mais nítido fica o gargalo de Von Neumann, ou seja, percebe-se que a performance do processador fica limitado a taxa de transferência de dados com a memória principal.
+Explain how a queue, implemented in hardware in the CPU, could be used to improve the performance of a write-through cache.
+Como observado na questão 1 cada momento que a escrita é debilitada, fica nítido o gargalo de Von Neuman se considerarmos que uma escrita na cache é uma escrita na memória principal, então cada Store iria demorar 50 nano segundos. Colocando uma fila e supondo que ela nunca fique cheia, a CPU não irá gastar tanto tempo no Store, mas uma vez a fila +cheia, a CPU terá que aguardar uma escrita na memória principal.
+Tempo em nanosegundos | +Fetch | +OP1 | +OP2 | +OP3 | +OP4 | +OP5 | +Store | +
---|---|---|---|---|---|---|---|
10 | +6 | +5 | +wait | +4 | +wait | +3 | +2 | +
11 | +6 | +wait | +5 | +wait | +4 | +wait | +3 | +
12 | +6 | +wait | +wait | +5 | +wait | +4 | +3 | +
13 | +6 | +wait | +wait | +wait | +5 | +4 | +3 | +
14 | +6 | +wait | +wait | +wait | +5 | +4 | +3 | +
15 | +7 | +6 | +wait | +wait | +5 | +4 | +3 | +
16 | +7 | +wait | +6 | +wait | +wait | +5 | +4 | +
Recall the example involving cache reads of a two-dimensional array (page 22). How does a larger matrix and a larger cache affect the performance of the two pairs of nested loops? What happens if MAX = 8 and the cache can store four lines ? How many misses occur in the reads of A in the first pair of nested loops ? How many misses occur in the second pair ?
+double A[MAX][MAX], x[MAX], y[MAX];
+
+//. . .
+// Initialize A and x, assign y = 0 ∗/
+//. . .
+
+
+/∗ First pair of loops ∗/
+for (i = 0; i < MAX; i++)
+ for (j = 0; j < MAX; j++)
+ y[i] += A[i][j]∗x[j];
+
+//. . .
+// Assign y = 0 ∗/
+//. . .
+
+/∗ Second pair of loops ∗/
+for (j = 0; j < MAX; j++)
+ for (i = 0; i < MAX; i++)
+ y[i] += A[i][j]∗x[j];
+
Cache line | +Elements of A | +
---|---|
0 | +A[0][0] A[0][1] A[0][2] A[0][3] | +
1 | +A[1][0] A[1][1] A[1][2] A[1][3] | +
2 | +A[2][0] A[2][1] A[2][2] A[2][3] | +
3 | +A[3][0] A[3][1] A[3][2] A[3][3] | +
Supondo que a cache tenha a mesma proporção do que a Matrix, o número de cache miss seria igual ao número de linhas da matriz, como apontado no exemplo dado no livro, quando o processador Pede o valor A[0][0], baseado na ideia de vizinhança, a cache carrega todas as outras colunas da linha 0, portanto é plausível pensar que o número de miss é igual ao número de linhas, ou seja, +o número miss é igual a MAX, pois a cache tem o mesmo número de linhas que a matrix A, suponto que não preciso me preocupar com x e y.
+Tendo a a cache armazenando metade dos valores de uma linha da Matriz A, então +para cada linha da Matriz, vai haver duas cache miss, a primeira np A[i][0] e a segunda no A[i][4]. Outro ponto é que como a cache só possui 4 linhas, então +após ocorrer os cache misses A[0][0] ,A[0][4] e A[1][0], A[1][4] toda a cache +terá sindo preenchida, ou seja, Tento a matriz 8 linhas e para cada linha tem 2 cache miss por tanto:
+ 8*2 =16 cache miss
+
como tanto a primeira parte quando na segunda parte, percorre-se todas as linhas +irá haver 16 cache miss, suponto que não preciso me preocupar com x e y.
+Cache line | +Elements of A | +
---|---|
0 | +A[0][0] A[0][1] A[0][2] A[0][3] | +
1 | +A[0][4] A[0][5] A[0][6] A[0][7] | +
2 | +A[1][0] A[1][1] A[1][2] A[1][3] | +
3 | +A[1][4] A[1][5] A[1][6] A[1][7] | +
No segundo par de loops, vemos que o segundo laço for, percorre os valores: +A[0][0], A[1][0], A[2][0] ... A[7][0], para quando j =0. Isso faz com que +todo hit seja miss, ou seja iremos ter miss para cada acesso em A, portanto:
+ 8*8 = 64 cache miss
+
Does the addition of cache and virtual memory to a von Neumann system change its designation as an SISD system ? What about the addition of +pipelining? Multiple issue? Hardware multithreading ?
+Um SISD system ou Single Instruction Single Data system, são sistemas que executam uma única instrução por vez e sua taxa de transferência de dados é +de um item por vez também.
+Adicionar um cache e memória virtual, pode ajudar a reduzir o tempo que única instrução é lida da memória principal, mas não aumenta o número de +instruções buscadas na operação Fetch ou na Operação Store, por tanto o sistema +continua sendo Single Instruction Single Data.
+Como demostrado na questão 1, ao adicionar um pipeline, podemos realizar a mesma instrução complexa em múltiplos dados, +ou seja, Single Instruction Multiple Data System, portanto sim.
+No momento em que possibilitamos uma máquina executar antecipadamente uma instrução ou possibilitamos a execução de múltiplas threads, nesse momento então a máquina está executando várias instruções em vários dados ao mesmo tempo, por tanto o sistema se torna Multiple Instruction Multiple Data system, ou seja, a designação muda.
+Discuss the differences in how a GPU and a vector processor might execute the following code: +
+Um processo de vetorização em cima desse laço for dividiria as entradas +em chucks ou blocos de dados e executaria em paralelo a instrução complexa. +Algo como:
+//executando o bloco paralelamente.
+y[0] += a∗x[0];
+y[1] += a∗x[1];
+y[2] += a∗x[2];
+y[3] += a∗x[3];
+
+z[0]∗z[0] // executando em paralelo
+z[1]∗z[1] // executando em paralelo
+z[2]∗z[2] // executando em paralelo
+z[3]∗z[3] // executando em paralelo
+// somando tudo depois
+sum+= z[0]∗z[0] + z[1]∗z[1] + z[2]∗z[2] + z[3]∗z[3];
+
Atualmente essa operação em GPU é muito mais interessante, pois hoje podemos +compilar ou gerar instruções complexas que podem ser executas em GPU. A primeira vantagem seria separar o calculo do sum:
+ sum += z[i]∗z[i];
+
do calculo do y
+ y[i] += a∗x[i];
+
ficando assim:
+# version 330
+
+layout(location = 0) in vec4 x;
+layout(location = 1) in mat4 a;
+layout(location = 2) in vec4 y;
+
+/* o buffer gl_Position é ré-inserido no y */
+void main()
+{
+ gl_Position = a*x + y;
+}
+//
+
# version 330
+
+layout(location = 0) in mat4 z;
+uniform float sum;
+
+/* transpose é uma função que calcula a transposta já existem no Glsl */
+void main()
+{
+ mat4 temp = transpose(z) * z;
+
+ sum = 0;
+ for (int i = 0; i < 4; i++)
+ // desde que o laço for seja baseado em constantes ou variáveis uniforms
+ // esse laço for é possível.
+ {
+ sum += temp[i];
+ }
+
+ // recupera o valor no index 0
+ gl_Position = vec4(sum, 0.0, 0.0, 0.0, 0.0);
+}
+
Suppose a program must execute \(10^{12}\) instructions in order to solve a particular problem. Suppose further that a single processor system can solve the problem in \(10^6\) seconds (about 11.6 days). So, on average, the single processor system executes \(10^6\) or a million instructions per second. Now suppose that the program has been parallelized for execution on a distributed-memory system. Suppose also that if the parallel program uses \(p\) processors, each processor will execute \(\frac{10^{12}}{p}\) instructions and each processor must send \(10^9( p − 1)\) messages. Finally, suppose that there is no additional overhead in executing the parallel program. That is, the program will complete after each processor has executed all of its instructions and sent all of its messages, and there won’t be any delays due to things such as waiting for messages.
+Suppose it takes \(10^{-9}\) seconds to send a message. How long will it take the program to run with 1000 processors, if each processor is as fast as the single processor on which the serial program was run ?
+Suppose it takes \(10^3\) seconds to send a message. How long will it take the program to run with 1000 processors ?
+import datetime
+
+NUMBER_OF_INSTRUCTIONS = 10**12
+NUMBER_OF_MESSAGES = 10**9
+AVERANGE_SECOND_PER_INSTRUCTIONS = (10**6) / NUMBER_OF_INSTRUCTIONS
+
+
+def cost_time_per_instruction(instructions: int) -> float:
+ return AVERANGE_SECOND_PER_INSTRUCTIONS * instructions
+
+
+def number_of_instructions_per_processor(p: int) -> int:
+ return NUMBER_OF_INSTRUCTIONS/p
+
+
+def number_of_messagens_per_processor(p: int) -> int:
+ return NUMBER_OF_MESSAGES * (p-1)
+
+
+def simulate(time_per_message_in_seconds: float, processors: int):
+ print(
+ f'time to send a message: {time_per_message_in_seconds} processors: {processors}')
+ instructions = number_of_instructions_per_processor(processors)
+ number_of_messages = number_of_messagens_per_processor(processors)
+ each_process_cost_in_seconds = cost_time_per_instruction(instructions)
+
+ total_messages_in_seconds = time_per_message_in_seconds * number_of_messages
+
+ result = total_messages_in_seconds + each_process_cost_in_seconds
+ result_date = datetime.timedelta(seconds=result)
+
+ print(f'executing instructions is {instructions}')
+ print(f'spend sending messages is {total_messages_in_seconds}')
+
+ print(f'total time in seconds: {result}')
+ print(f'total time in HH:MM:SS {result_date}')
+
+
+def a():
+
+ time_per_message_in_seconds = 1e-9
+ processors = 1e3
+ simulate(time_per_message_in_seconds, processors)
+
+
+def b():
+ time_per_message_in_seconds = 1e-3
+ processors = 1e3
+ simulate(time_per_message_in_seconds, processors)
+
+
+def main():
+ print('A:')
+ a()
+ print('B:')
+ b()
+
+
+if __name__ == '__main__':
+ main()
+
python chapter_2/question_10/main.py
+A:
+time to send a message: 1e-09 processors: 1000.0
+executing instructions is 1000000000.0
+spend sending messages is 999.0000000000001
+total time in seconds: 1999.0
+total time in HH:MM:SS 0:33:19
+B:
+time to send a message: 0.001 processors: 1000.0
+executing instructions is 1000000000.0
+spend sending messages is 999000000.0
+total time in seconds: 999001000.0
+total time in HH:MM:SS 11562 days, 12:16:40
+
Suppose a shared-memory system uses snooping cache coherence and +write-back caches. Also suppose that core 0 has the variable x in its cache, and it executes the assignment x = 5. Finally suppose that core 1 doesn’t have x in its cache, and after core 0’s update to x, core 1 tries to execute y = x. What value will be assigned to y ? Why ?
+Suppose that the shared-memory system in the previous part uses a +directory-based protocol. What value will be assigned to y ? Why ?
+Can you suggest how any problems you found in the first two parts might be solved ?
+Não é possível determinar qual valor será atribuído ao y independentemente se for write-back ou write-through, uma vez que não +houve uma sincronização entre os cores sobre o valor de x. A atribuição +de y = x do core 1 pode ocorrer antes ou depois das operações no core 0.
+Com o sistema de arquivos, ao core 0 irá notificar o a memória principal que a consistência dos dados foi comprometida, no entanto, ainda não dá +para saber qual o valor de y, uma vez que a atribuição de y = x do core 1 pode ocorrer antes ou depois das operações no core 0.
+Existe dois problemas, o problema da consistência do dados, temos que garantir que ambos os cores façam alterações que ambas sejam capaz de +ler e o segundo é um mecanismo de sincronização, onde por exemplo, o core 1 espera o core 0 finalizar o seu processamento com a variável x para ai sim começar o seu. Podemos utilizar por exemplo um mutex, onde +inicialmente o core 0 faria o lock e ao finalizar ele entrega a chave a qual, o core 1 pegaria.
+a. Suppose the run-time of a serial program is given by \(T_{serial} = n^2\) , where the units of the run-time are in microseconds. Suppose that a parallelization of this program has run-time \(T_{parallel} = \frac{n^2}{p} + log_2(p)\). Write a program that finds the speedups and efficiencies of this program for various values of n and p. Run your program with \(n = 10, 20, 40, . . . , 320\), and \(p = 1, 2, 4, . . . , 128\). What happens to the speedups and efficiencies as \(p\) is increased and \(n\) is held fixed? What happens when \(p\) is fixed and \(n\) is increased ?
+b. Suppose that \(T_{parallel} = \frac{T_{serial}}{p} + T_{overhead}\) . Also suppose that we fix \(p\) and increase the problem size.
+Podemos observar a Amadahl's law, "a lei de Amadahl diz: a menos que virtualmente todo o programa serial seja paralelizado, o possível speedup, ou ganhado de performance será muito limitado, independentemente dos números de cores disponíveis." Nó exemplo em questão, podemos observar que a partir de +32 cores, o tempo da aplicação fica estagnado por volta dos 1606 micro segundos.
+Mesmo com da lei de Amdahl, podemos observar que o aumento de performance é bastante significante, valendo a pena paralelizar a aplicação, também pelo fato que os hardwares atualmente possuem mais +de um core.
+Sabendo que o tempo serial é o quadrático da entrada, considerei o +tempo de overhead sendo: \(T_{overhead} = 5n\), ou seja, uma função linear. Comparando com o gráfico da letra a dificilmente é notado uma diferença entre os gráficos, podendo até ser desconsiderado.
+Sabendo que o tempo serial é o quadrático da entrada, considerei o +tempo de overhead sendo: \(T_{overhead} = 3T_{serial} = 3n^2\), ou seja, o overhead cresce 3 vezes mais que o serial. Comparando com o gráfico da letra a podemos observar que paralelizar não é uma boa opção, pois +nem com \(p =128\) consegue ser melhor do que com \(p =1\), ou seja, não paralelizar, ou seja, a solução serial sem o overhead.
+Para essa atividade foi utilizado Python na sua versão 3.10.4 +para instalar as dependências do script e criar os gráficos:
+ +A parallel program that obtains a speedup greater than p—the number of processes or threads—is sometimes said to have superlinear speedup. However, many authors don’t count programs that overcome “resource limitations” as having superlinear speedup. For example, a program that must use secondary storage for its data when it’s run on a single processor system might be able to fit all its data into main memory when run on a large distributed-memory system. Give another example of how a program might overcome a resource limitation and obtain speedups greater than p
+Sistemas embarcados com CPUs multi-core. Atualmente já existem microcontroladores como ESP32 que dependendo do modelo pode possuir +mais de um núcleo de processamento. Sabendo que todo o programa fica carregado na memória, então uma aplicação como um servidor HTTP, pode +ter mais que o dobro de performance, quando observado o seu requests por segundo.
+Fazemos o seguinte cenário:
+Temos um desenvolvedor que sabe que executar operações de escrita em hardware é uma operação insegura e utiliza estruturas de +dados para sincronização dessas operações, cada dispositivo tem seu tempo de sincronização. Temos que o dispositivo A custa 5 milissegundos, +e dispositivo B custa 4 milissegundos. Sabendo que se criarmos a aplicação em single core, temos que esperar a sincronização de A e B e que +modificar o tempo de sincronização de um dispositivo custa 3 milissegundos. Dado que se tem 2 cores, 2 conversores AD, se delegarmos cada dispositivo +para um core, eliminaremos 3 milissegundos do processo de escrita no seu pior cenário. Supondo que o tempo de um request fique 30% na escrita de +um dispositivo e que ele gasta em outras operações 8 milissegundos temos dois cenários,
+\(R_{e}=0.7(8) + 0.3(5+3)\)
+\(R_{e}=8\), ou seja uma Request de escrita custa 8 milissegundos
+\(R_{e}=0.7(8) + 0.3(5)\)
+\(R_{e}=7.1\), ou seja uma Request de escrita custa 7.1 milissegundos, além do fato do ESP32 ser capaz de realizar o dobro de requests
+Suppose \(T_{serial} = n\) and \(T_{parallel} = \frac{n}{p} + log_2 (p)\), where times are in microseconds. If we increase \(p\) by a factor of \(k\), find a formula for how much we’ll need to increase n in order to maintain constant efficiency. How much should we increase \(n\) by if we double the number of processes from 8 to 16 ? Is the parallel program scalable ?
+\(E(p) = \frac{T_{serial}}{pT_{parallel}}\)
+\(E(p) = \frac{n}{p(\frac{n}{p} + log_2(p))}\)
+\(E(p) = \frac{n}{n + plog_2(p)}\)
+\(E(kp) = \frac{n}{n + kplog_2(kp)}\)
+Se considerarmos a constante \(A\) o número de vezes que temos que aumentar \(n\) para +obter uma eficiência constante, logo:
+\(E_{a} (kp) = \frac{An}{An + kplog_2(kp)}\)
+\(E_{a}(kp) = E(p)\)
+\(\frac{An}{An + kplog_2(kp)} = \frac{n}{n + plog_2(p)}\)
+\(An =\frac{n(nA + kplog_2(kp))}{n + plog_2(p)}\)
+\(A = \frac{nA + kplog_2(kp)}{n + plog_2(p)}\)
+\(A = \frac{nA}{n + plog_2(p)} + \frac{ kplog_2(kp)}{n + plog_2(p)}\)
+\(A - \frac{nA}{n + plog_2(p)}= \frac{ kplog_2(kp)}{n + plog_2(p)}\)
+\(A[ 1 - \frac{n}{n + plog_2(p)}]= \frac{ kplog_2(kp)}{n + plog_2(p)}\)
+\(A[\frac{n + plog_2(p)}{n + plog_2(p)} - \frac{n}{n + plog_2(p)}]= \frac{ kplog_2(kp)}{n + plog_2(p)}\)
+\(A\frac{plog_2(p)}{n + plog_2(p)} = \frac{ kplog_2(kp)}{n + plog_2(p)}\)
+\(Aplog_2(p) = kplog_2(kp)\)
+\(A = \frac{kplog_2(kp)}{plog_2(p)}\)
+\(A = \frac{klog_2(kp)}{log_2(p)}\)
+\(A(k,p) =\frac{klog_2(kp)}{log_2(p)}\) +se \(k = 2\) e \(p=8\) então:
+\(A = \frac{2log_{2}(16)}{log_{2}(8)}\)
+\(A = \frac{2(4)}{3}\)
+\(A = \frac{8}{3}\)
+Dado a definição do autor sim. Para o autor escalável é quando a eficiência de um programa paralelo se mantém constante, ou seja, se existe uma taxa que relaciona o crescimento do tamanho do problema com o crescimento do número de threads/processos, então o programa paralelo é fracamente escalável (weakly scalable), .
+Is a program that obtains linear speedup strongly scalable ? Explain your answer.
+Dado a definição do autor sim. Para o autor escalável é quando a eficiência de um programa paralelo se mantém constante. Linear speedup +pode ser escrito pela seguinte expressão:
+\(S = \frac{T_{serial}}{T_{parallel}} = p\), onde \(p\) é número de cores e \(S\) é o speedup.
+Portanto dado que eficiência é dado pela seguinte expressão:
+\(E = \frac{T_{serial}}{pT_{parallel}}\), onde \(T_{serial}\) é o tempo da aplicação em serial e \(T_{parallel}\) o tempo da aplicação em paralelizada.
+se o speedup for linear, ou seja, \(S=p\), temos que
+\(E = \frac{S}{p}\), portanto
+\(E = \frac{p}{p} = 1\), Como a eficiência é constante, logo, por definição a aplicação é fortemente escalável (strongly scalable).
+Bob has a program that he wants to time with two sets of data, input_data1 and input_data2. To get some idea of what to expect before adding timing functions to the code he’s interested in, he runs the program with two sets of data and the Unix shell command time: +
$ time ./bobs prog < input data1
+real 0m0.001s
+user 0m0.001s
+sys 0m0.000s
+$ time ./bobs prog < input data2
+real 1m1.234s
+user 1m0.001s
+sys 0m0.111s
+
Segundo a referência o commando time, retorna três valores:
+Na primeira chamada observamos que o tempo coletado é praticamente 0, ou seja, o tempo levado para executar o programa esta fora da resolução do relógio do sistema, por tanto não podemos concluir nada sobre a primeira chamada e se essa for a primeira chamada é bem provável que a próxima também dê praticamente 0, uma vez que a aplicação pode ter pouco tamanho +de entrada se comparado a máquina em questão.
+Já no segundo timer podemos observar informações sobre a aplicação, estão dentro da resolução do relógio e que maior parte da aplicação foi gasta em em user mode. Bob pode fazer proveito dessas informações.
+If you haven’t already done so in Chapter 1, try to write pseudo-code for our tree-structured global sum, which sums the elements of loc_bin_cts. First consider how this might be done in a shared-memory setting. Then consider how this might be done in a distributed-memory setting. In the shared-memory setting, which variables are shared and which are private ?
+Um código em C++, mas sem o uso de estruturas de dados de programação paralela, pode ser observado na resposta da questão 5 Question_5.cpp. Também foi implementado o mesmo algoritmo em rust e nessa implementação foi utilizado threads, smart pointers e mutex para +resolver o problema. O código pode ser observado aqui: main.rs
+use std::{
+ sync::{Arc, Mutex},
+ thread::JoinHandle,
+};
+
+#[derive(Debug)]
+struct Node {
+ data: Vec<i32>,
+ neighborhoods: Mutex<Vec<JoinHandle<i32>>>,
+}
+
+impl Node {
+ fn from_value(data: i32) -> Node {
+ Node {
+ data: vec![data],
+ neighborhoods: Mutex::new(vec![]),
+ }
+ }
+
+ fn compute(self) -> i32 {
+ /*
+ Em termos de memória essa função desaloca toda memória
+ usada pela estrutura Node e retorna um inteiro de 32bits.
+ Dado que foi utilizado safe rust e o código compila, logo esse
+ código está livre data race e como não referências cíclicas
+ também está livre de memory leak.
+ */
+ let result: i32 = self.data.iter().sum();
+
+ let neighborhoods = self.neighborhoods.into_inner().unwrap();
+
+ let neighborhoods_sum: i32 = neighborhoods
+ .into_iter()
+ .map(|handle| handle.join().expect("Unable to lock neighborhood"))
+ .sum();
+
+ result + neighborhoods_sum
+ }
+}
+
+fn start_to_compute_node(node: Node) -> JoinHandle<i32> {
+ std::thread::spawn(move || {
+ let result = node.compute();
+ std::thread::sleep(std::time::Duration::from_micros(500));
+
+ result
+ })
+}
+
+fn receive_value(left: Arc<Node>, right: Arc<Node>) {
+ let right = Arc::try_unwrap(right).unwrap();
+
+ let mut left_neighborhoods = left
+ .neighborhoods
+ .lock()
+ .expect("Unable to lock neighborhood");
+
+ left_neighborhoods.push(start_to_compute_node(right))
+}
+
+fn create_new_tree_bitwise(mut nodes: Vec<Arc<Node>>) -> Vec<Arc<Node>> {
+ let size = nodes.len();
+
+ match size {
+ 2 => {
+ let left = nodes.remove(0);
+ let right = nodes.remove(0);
+
+ receive_value(left.clone(), right);
+
+ vec![left]
+ }
+
+ 3 => {
+ let left = nodes.remove(0);
+ let middle = nodes.remove(0);
+ let right = nodes.remove(0);
+
+ receive_value(left.clone(), middle);
+ receive_value(left.clone(), right);
+
+ vec![left]
+ }
+
+ _ => {
+ let mut new_nodes = vec![];
+
+ let mut size = size;
+
+ if size % 2 != 0 {
+ size = size - 1;
+
+ new_nodes.push(nodes.remove(size - 1));
+ }
+
+ for i in (0..size).step_by(2) {
+ let left = nodes.remove(0);
+ let right = nodes.remove(0);
+ println!("i: {} left: {:?} right: {:?}", i, left, right);
+ receive_value(left.clone(), right);
+ new_nodes.push(left);
+ }
+ println!("Next iteration");
+
+ create_new_tree_bitwise(new_nodes)
+ }
+ }
+}
+
+fn main() {
+ let data = vec![8, 19, 7, 15, 7, 13, 12, 14];
+
+ let nodes: Vec<Arc<Node>> = data
+ .clone()
+ .iter()
+ .map(|&v| Node::from_value(v))
+ .map(|node| Arc::new(node))
+ .collect();
+
+ let root = create_new_tree_bitwise(nodes)[0].clone();
+
+ let root = Arc::try_unwrap(root).unwrap();
+
+ let total = root.compute();
+
+ assert!( total == data.iter().sum::<i32>());
+ println!(
+ "total: {} data sum {} ",
+ total,
+ data.iter().sum::<i32>()
+ );
+}
+
+#[test]
+fn tree_sum() {
+ /*
+ Teste com várias entradas
+ */
+ let data_tests = vec![
+ vec![8, 19, 7, 15, 7, 13, 12, 14],
+ vec![8, 19, 7, 15, 7, 13, 12],
+ vec![8, 19, 7, 15, 7, 13],
+ vec![8, 19, 7, 15, 7],
+ vec![8, 19, 7, 15],
+ vec![8, 19, 7],
+ vec![8, 19],
+ ];
+
+ for data in data_tests {
+ let nodes: Vec<Arc<Node>> = data
+ .clone()
+ .iter()
+ .map(|&v| Node::from_value(v))
+ .map(|node| Arc::new(node))
+ .collect();
+
+ let root = create_new_tree_bitwise(nodes)[0].clone();
+
+ let root = Arc::try_unwrap(root).unwrap();
+
+ assert_eq!(root.compute(), data.iter().sum::<i32>());
+ }
+}
+
caso tenha o rust instalado você pode observar a execução do caso de teste +com o comando cargo.
+ +O teste é mesmo teste feito em c++.
+Capítulo 3: 2, 4, 6, 9, 11, 12, 13, 16, 17, 19, 20, 22, 23, 27 e 28 (16 questões);
+Modify the program that just prints a line of output from each process (mpi_output.c) so that the output is printed in process rank order: process 0s output first, then process 1s, and so on.
+Suppose comm_sz = 4 and suppose that x is a vector with \(n = 14\) components.
+Finding prefix sums is a generalization of global sum. Rather than simply finding the sum of \(n\) values,
+\(x_0 + x_1 + \cdot \cdot \cdot + x_{n-1}\)
+the prefix sums are the n partial sums
+\(x_0, x_0 + x_1, x_0 + x_1 + x_2, \cdot \cdot \cdot, x_0 + x_1 \cdot \cdot \cdot + x_{n-1}\)
An alternative to a butterfly-structured allreduce is a ring-pass structure. In a ring-pass, if there are \(p\) processes, each process \(q\) sends data to process \(q + 1\), except that process \(p − 1\) sends data to process \(0\). This is repeated until each process has the desired result. Thus, we can implement allreduce with the following code: +
sum = temp val = my val;
+
+for (i = 1; i < p; i++) {
+ MPI Sendrecv replace(&temp val, 1, MPI INT, dest,
+ sendtag, source, recvtag, comm, &status);
+ sum += temp val;
+}
+
MPI Scatter and MPI Gather have the limitation that each process must send or receive the same number of data items. When this is not the case, we must use the MPI functions MPI Gatherv and MPI Scatterv. Look at the man pages for these functions, and modify your vector sum, dot product program so that it can correctly handle the case when n isn’t evenly divisible by +comm_sz.
+Suppose comm_sz \(= 8\) and the vector x \(= (0, 1, 2, . . . , 15)\) has been distributed among the processes using a block distribution. Draw a diagram illustrating the steps in a butterfly implementation of allgather of x
+MPI_Type_contiguous can be used to build a derived datatype from a collection of contiguous elements in an array. Its syntax is +
+ Modify the Read_vector and Print_vector functions so that they use an MPI datatype created by a call to MPI_Type_contiguous and a count argument of 1 in the calls to MPI_Scatter and MPI_Gather. +MPI_Type_indexed can be used to build a derived datatype from arbitrary array elements. Its syntax is +
int MPI_Type_indexed(
+ int count, /* in */
+ int array_of_blocklengths[], /* in */,
+ int array_of_displacements[], /* in */,
+ MPI_Datatype old_mpi_t, /* in */
+ MPI_Datatype∗ new_mpi_t_p /* out */);
+
the upper triangular part is the elements 0, 1, 2, 3, 5, 6, 7, 10, 11, 15. Process +0 should read in an \(n \times n\) matrix as a one-dimensional array, create the derived datatype, and send the upper triangular part with a single call to MPI_Send. Process 1 should receive the upper triangular part with a single call ot MPI_Recv and then print +the data it received.
+The functions MPI_Pack and MPI_Unpack provide an alternative to derived datatypes for grouping data. MPI_Pack copies the data to be sent, one block at a time, into a user-provided buffer. The buffer can then be sent and received. After the data is received, MPI_Unpack can be used to unpack it from the receive buffer. The syntax of MPI_Pack is +
int MPI_Pack(
+ void* in_buf, /* in */
+ int in_buf_count, /* in */
+ MPI_Datatype datatype, /* in */
+ void* pack_buf, /* out */
+ int pack_buf_sz, /* in */
+ int* position_p, /* in/out */
+ MPI_Comm comm /* in */);
+
Time our implementation of the trapezoidal rule that uses MPI_Reduce. How will + you choose n, the number of trapezoids ? How do the minimum times compare to the mean and median times ? What are the speedups ? What are the efficiencies ? On the basis of the data you collected, would you say that the trapezoidal rule is scalable ?
+Although we don’t know the internals of the implementation of MPI_Reduce, we might guess that it uses a structure similar to the binary tree we discussed. If this is the case, we would expect that its run-time would grow roughly at the rate of \(log_2(p)\), since there are roughly \(log_2(p)\) levels in the tree. (Here, \(p =\)comm_sz.) Since the run-time of the serial trapezoidal rule is roughly proportional to \(n\), the number of trapezoids, and the parallel trapezoidal rule simply applies the serial rule to \(\frac{n}{p}\) trapezoids on each process, with our assumption about MPI_Reduce, we get a formula for the overall run-time of the parallel trapezoidal rule that looks like
+\(T_{parallel}(n,p) \approx a \times \frac{n}{p} + blog_2(p)\)
+ for some constants \(a\) and \(b\).
Find the speedups and efficiencies of the parallel odd-even sort. Does the program obtain linear speedups? Is it scalable ? Is it strongly scalable ? Is it weakly scalable ?
+Modify the parallel odd-even transposition sort so that the Merge functions simply swap array pointers after finding the smallest or largest elements. What effect does this change have on the overall run-time ?
+Quando o compilador não é capaz de vetorizar automaticamente, ou vetoriza de forma ineficiente, o OpenMP provê a diretiva omp simd, com a qual o programador pode indicar um laço explicitamente para o compilador vetorizar. No código abaixo, a inclusão da cláusula reduction funciona de forma similar a flag -ffast-math, indicando que a redução na variável soma é segura e deve ser feita. +
+Por que não é necessário usar a cláusula private(x) neste caso mas seria caso a diretiva omp simd fosse combinada com a diretiva omp parallel for ? +Modify the trapezoidal rule so that it will correctly estimate the integral even if comm_sz doesn’t evenly divide n. (You can still assume that n \(\geq\) comm_sz.).
+#include <mpi.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+
+double trap(double a, double b, long int n);
+
+double function(double x);
+
+int main(int argc, char *argv[])
+{
+
+ int my_rank;
+ int comm_sz;
+
+ int message_tag = 0;
+ // resceber argumentos globais via linha de commando
+ MPI_Init(&argc, &argv);
+
+ const double A = atof(argv[1]);
+ const double B = atof(argv[2]);
+ const int N = atoi(argv[3]);
+
+
+
+ MPI_Comm_rank(MPI_COMM_WORLD, &my_rank);
+ MPI_Comm_size(MPI_COMM_WORLD, &comm_sz);
+
+ double h = (B - A) / N;
+
+ int local_n = N / comm_sz;
+ /*
+ Como podemos observar que quando a divisão
+ inteira N/comm_sz o n local, não corresponde
+ ao número total de trapézios desejados.
+ Ex: 1024/3 == 341, sendo que 341*3 == 1023
+ */
+ double local_a = A + my_rank * local_n * h´;
+ double local_b = local_a + local_n * h;
+ double result_integral = trap(local_a, local_b, local_n);
+
+ if (my_rank != 0)
+ {
+ MPI_Send(&result_integral, 1, MPI_DOUBLE, 0, message_tag, MPI_COMM_WORLD);
+ }
+ else // my_rank ==0
+ {
+
+ for (int source = 1; source < comm_sz; source++)
+ {
+ double result_integral_source;
+ MPI_Recv(&result_integral_source, 1, MPI_DOUBLE, source, message_tag, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
+ result_integral += result_integral_source;
+ }
+ printf("with n = %d trapezoids, our estimate\n", N);
+ printf("of the integral from %f to %f = %.15e\n", A, B, result_integral);
+ }
+
+ MPI_Finalize();
+
+ return 0;
+}
+
+double trap(double a, double b, long int n)
+{
+
+ double h = (b - a) / n;
+
+ double approx = (function(a) + function(b)) / 2.0;
+ for (int i = 1; i < n - 1; i++)
+ {
+ // printf("f_%i \n", i);
+ double x_i = a + i * h;
+
+ approx += function(x_i);
+ }
+
+ return h * approx;
+}
+
+double function(double x) { return x * x; }
+
Sabendo que pode ocorrer divisões cujo o número não pertence aos números racionais, por tanto se faz necessário utilizar a seguinte +expressão: +$$ + \frac{N}{D} = a \times D +\text{resto} +$$ +exemplo: +$$ + \frac{1024}{3} = 341 \times 3 + 1 +$$ +Podemos resolver o problema simplesmente despejando os trapézios restantes em um processo ou podemos dividir a "carga" entre os processos. Dividir a carga entre os processos de forma mais igualitária possível, foi resolvido no exercício 1 capítulo 1.
+struct range
+{
+ int first;
+ int last;
+};
+
+struct range new_range_2(int thread_index, int p, int n)
+{
+ struct range r;
+
+ int division = n / p;
+ int rest = n % p;
+
+ if (thread_index < rest)
+ {
+ r.first = thread_index * (division + 1);
+ r.last = r.first + division + 1;
+ }
+ else
+ {
+ r.first = thread_index * division + rest;
+ r.last = r.first + division;
+ }
+
+ return r;
+}
+...
+
+int main(int argc, char *argv[])
+{
+
+ ...
+ /*
+ onde thread_index é o rank,
+ o número de cores é o número de processos,
+ o tamanho do vetor é o número de trapézios
+ */
+ struct range r = new_range_2(my_rank,comm_sz,N);
+
+ double h = (B - A) / N;
+
+ /*
+ perceba que o número local de trapézios é
+ o tamanho do intervalo calculado
+ */
+ int local_n = r.last -r.first;
+ double local_a = A + r.first * h;
+ double local_b = A + r.last * h;
+ double result_integral = trap(local_a, local_b, local_n);
+
+ printf("local n: %i local a: %f local b %f \n", local_n, local_a, local_b);
+}
+
mpicc trap_rule.c -o trap; mpiexec -n 3 ./trap 1 3 1024
+local n: 342 local a: 1.000000 local b 1.667969
+local n: 341 local a: 1.667969 local b 2.333984
+local n: 341 local a: 2.333984 local b 3.000000
+with n = 1024 trapezoids, our estimate
+of the integral from 1.000000 to 3.000000 = 8.633069768548012e+00
+
mpicc trap_rule.c -o trap; mpiexec -n 4 ./trap 1 3 2024
+local n: 506 local a: 1.000000 local b 1.500000
+local n: 506 local a: 1.500000 local b 2.000000
+local n: 506 local a: 2.000000 local b 2.500000
+local n: 506 local a: 2.500000 local b 3.000000
+with n = 2024 trapezoids, our estimate
+of the integral from 1.000000 to 3.000000 = 8.645439504647232e+00
+
mpicc trap_rule.c -o trap; mpiexec -n 3 ./trap 0 3 1024
+local n: 342 local a: 0.000000 local b 1.001953
+local n: 341 local a: 1.001953 local b 2.000977
+local n: 341 local a: 2.000977 local b 3.000000
+with n = 1024 trapezoids, our estimate
+of the integral from 0.000000 to 3.000000 = 8.959068736061454e+00
+
#include <mpi.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+struct range
+{
+ int first;
+ int last;
+};
+
+struct range new_range_2(int thread_index, int p, int n)
+{
+ struct range r;
+
+ int division = n / p;
+ int rest = n % p;
+
+ if (thread_index < rest)
+ {
+ r.first = thread_index * (division + 1);
+ r.last = r.first + division + 1;
+ }
+ else
+ {
+ r.first = thread_index * division + rest;
+ r.last = r.first + division;
+ }
+
+ return r;
+}
+``
+double trap(double a, double b, long int n);
+
+double function(double x);
+
+int main(int argc, char *argv[])
+{
+
+ int my_rank;
+ int comm_sz;
+
+ int message_tag = 0;
+
+ MPI_Init(&argc, &argv);
+
+ const double A = atof(argv[1]);
+ const double B = atof(argv[2]);
+ const int N = atoi(argv[3]);
+
+ MPI_Comm_rank(MPI_COMM_WORLD, &my_rank);
+ MPI_Comm_size(MPI_COMM_WORLD, &comm_sz);
+
+ struct range r = new_range_2(my_rank, comm_sz, N);
+
+ double h = (B - A) / N;
+
+ int local_n = r.last - r.first;
+ double local_a = A + r.first * h;
+ double local_b = A + r.last * h;
+ double result_integral = trap(local_a, local_b, local_n);
+
+ printf("local n: %i local a: %f local b %f \n", local_n, local_a, local_b);
+
+ if (my_rank != 0)
+ {
+ MPI_Send(&result_integral, 1, MPI_DOUBLE, 0, message_tag, MPI_COMM_WORLD);
+ }
+ else // my_rank ==0
+ {
+
+ for (int source = 1; source < comm_sz; source++)
+ {
+ double result_integral_source;
+ MPI_Recv(&result_integral_source, 1, MPI_DOUBLE, source, message_tag, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
+ result_integral += result_integral_source;
+ }
+ printf("with n = %d trapezoids, our estimate\n", N);
+ printf("of the integral from %f to %f = %.15e\n", A, B, result_integral);
+ }
+
+ MPI_Finalize();
+
+ return 0;
+}
+
+double trap(double a, double b, long int n)
+{
+
+ double h = (b - a) / n;
+ // printf("H: %f\n",h);
+
+ double approx = (function(a) + function(b)) / 2.0;
+ for (int i = 1; i < n - 1; i++)
+ {
+ // printf("f_%i \n", i);
+ double x_i = a + i * h;
+
+ approx += function(x_i);
+ }
+
+ return h * approx;
+}
+
+double function(double x) { return x * x; }
+
Copyright © 2022 Samuel Cavalcanti
+ +Capítulo 1: 1-6, 9; (7 questões)
+Devise formulas for the functions that calculate my_first_i and my_last_i in the global sum example. Remember that each core should be assigned roughly the same number of elements of computations in the loop. Hint: First consider the case when n is evenly divisible by p
+ +We’ve implicitly assumed that each call to Compute_next_value requires roughly the same amount of work as the other calls. How would you change your answer to the preceding question if call i = k requires k + 1 times as much work as the call with i = 0? So if the first call (i = 0) requires 2 milliseconds, the second call (i = 1) requires 4, the third (i = 2) requires 6, and so on.
+ +Try to write pseudo-code for the tree-structured global sum illustrated in +Figure 1.1. Assume the number of cores is a power of two (1, 2, 4, 8, . . . ).
+ +As an alternative to the approach outlined in the preceding problem we can use C’s bitwise operators to implement the tree-structured global sum. In order to see how this works, it helps to write down the binary (base 2) representation of each of the core ranks, and note the pairings during each stage
+ +What happens if your pseudo-code in Exercise 1.3 or Exercise 1.4 is run when the number of cores is not a power of two (e.g., 3, 5, 6, 7) ? Can you modify the +pseudo-code so that it will work correctly regardless of the number of cores ?
+ +Derive formulas for the number of receives and additions that core 0 carries out using:
+ a. the original pseudo-code for a global sum
+ b. the tree-structured global sum.
+Make a table showing the numbers of receives and additions carried out by core
+0 when the two sums are used with 2, 4, 8, . . . , 1024 cores.
Write an essay describing a research problem in your major that would benefit from the use of parallel computing. Provide a rough outline of how parallelism would be used. Would you use task- or data-parallelism ?
+ +Devise formulas for the functions that calculate my_first_i and my_last_i in the global sum example. Remember that each core should be assigned roughly the same number of elements of computations in the loop. Hint: First consider the case when n is evenly divisible by p
+struct range
+{
+ int first;
+ int last;
+};
+struct range new_range(int thread_index, int p, int n)
+{
+ struct range r;
+
+ int division = n / p;
+ int rest = n % p;
+
+ if (rest == 0)
+ {
+ r.first = thread_index * division;
+ r.last = (thread_index + 1) * division;
+ }
+ else
+ {
+ r.first = thread_index == 0 ? 0 : thread_index * division + rest;
+ r.last = (thread_index + 1) * division + rest;
+ }
+
+ if (r.last > n)
+ r.last = n;
+
+ return r;
+}
+
+struct range new_range_2(int thread_index, int p, int n)
+{
+ struct range r;
+
+ int division = n / p;
+ int rest = n % p;
+
+ if (thread_index < rest)
+ {
+ r.first = thread_index * (division + 1);
+ r.last = r.first + division + 1;
+ }
+ else
+ {
+ r.first = thread_index * division + rest;
+ r.last = r.first + division;
+ }
+
+ return r;
+}
+
First 0 Last 20 m Last - First: 20
+First 20 Last 40 m Last - First: 20
+First 40 Last 60 m Last - First: 20
+First 60 Last 80 m Last - First: 20
+First 80 Last 100 m Last - First: 20
+First 0 Last 25 m Last - First: 25
+First 25 Last 50 m Last - First: 25
+First 50 Last 75 m Last - First: 25
+First 75 Last 99 m Last - First: 24
+First 99 Last 123 m Last - First: 24
+Test question 1 success
+OLD new range
+First 0 Last 27 m Last - First: 27
+First 27 Last 51 m Last - First: 24
+First 51 Last 75 m Last - First: 24
+First 75 Last 99 m Last - First: 24
+First 99 Last 123 m Last - First: 24
+
We’ve implicitly assumed that each call to Compute_next_value requires roughly the same amount of work as the other calls. How would you change your answer to the preceding question if call i = k requires k + 1 times as much work as the call with i = 0? So if the first call (i = 0) requires 2 milliseconds, the second call (i = 1) requires 4, the third (i = 2) requires 6, and so on.
+Exemplo, Supondo que k = 10, temos o seguinte vetor de índice:
+indices = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
+utilizando a lógica da somatório de gauss e organizando os indicies temos um array normalizado:
+normalized_array = [ [0, 9], [1, 8], [2, 7], [3, 6], [4, 5] ]
+onde o custo de cada índice do normalized_array é igual, por tanto
+podemos usar o algoritmo da questão 1 aplicado ao normalized_array
+resultando:
Thread | +normalized_array | +
---|---|
1 | +0,1 | +
2 | +2,3 | +
3 | +4 | +
Thread | +Compute_next_value | +cost | +
---|---|---|
1 | +0, 9,1, 8 | +44 | +
2 | +2, 7,3, 6 | +44 | +
3 | +4, 5 | +22 | +
Try to write pseudo-code for the tree-structured global sum illustrated in +Figure 1.1. Assume the number of cores is a power of two (1, 2, 4, 8, . . . ).
+Para criar a árvore, foi considerado que o vetor principal já foi igualmente espaçado entre as p threads, usando o algoritmo da questão 1.
+Neste caso foi representado o nó, como uma estrutura que possui um vetor de vizinhos e outro ponteiro para um vetor de inteiros, na prática, +o ponteiro para um vetor de inteiros, seria usando o design pattern chamado Future, ou um Option\<Future>.
+Também foi criado dois construtores um construtor que representa, +a inicialização do Nó por meio dos seus vizinhos a esquerda e direita, +usando na criação de nós intermediários da árvore, e a inicialização +do Nó por meio do seu dado, os nós inicializados por meio dos dados +abstrai os núcleos ou as threads que estão sendo usadas para +executar o algoritmo de alto custo computacional.
+class Node
+{
+public:
+ std::vector<Node *> neighborhoods;
+ std::vector<int> *data;
+ Node(Node *left, Node *right)
+ {
+ this->neighborhoods.push_back(left);
+ this->neighborhoods.push_back(right);
+ this->data = nullptr;
+ }
+ Node(std::vector<int> *data)
+ {
+ this->data = data;
+ }
+ ~Node()
+ {
+ delete this->data;
+ }
+};
+
Para criar a Árvore foi feita uma função recursiva que +a partir do nível mais baixo da árvore cria a raiz, ou seja, +a partir um vetor com p Nós ,a função vai sendo chamada recursivamente, +onde a cada chamada vai-se criando um nível acima da árvore, até +que se atinja a raiz, onde a cada nível o número de nós é dividido +por 2. Caso o número de nós inicial não for divisível pro 2, o algoritmo não funciona
+std::vector<Node *> create_tree_from_core_nodes(std::vector<Node *> nodes)
+{
+ auto size = nodes.size();
+
+ if (size / 2 == 1)
+ {
+ auto left = nodes[0];
+ auto right = nodes[1];
+ receive_value(left, right); // Left receive value from right
+ return {left};
+ }
+
+ auto new_nodes = std::vector<Node *>{};
+
+ for (auto i = 0; i < size; i += 2)
+ {
+ auto left = nodes[i];
+ auto right = nodes[i + 1];
+ receive_value(left, right); // Left receive value from right
+ new_nodes.push_back(left);
+ }
+
+ return create_tree_from_core_nodes(new_nodes);
+}
+
+Node *create_new_tree(std::vector<Node *> nodes)
+{
+ return create_tree_from_core_nodes(nodes)[0];
+}
+
Após criar a árvore basta percorrer-lá recursivamente, lembrando que na prática +compute_data, seria um join, ou um await, de uma thread.
+int compute_data(std::vector<int> *data)
+{
+ auto total = 0;
+ auto size = data->size();
+ for (auto i = 0; i < size; i++)
+ {
+ total += data->at(i);
+ }
+
+ return total;
+}
+int compute_node(Node &node)
+{
+
+ int result_data = node.data == nullptr ? 0 : compute_data(node.data);
+
+ for (auto neighborhood : node.neighborhoods)
+ result_data += compute_node(*neighborhood);
+
+ return result_data;
+}
+
As an alternative to the approach outlined in the preceding problem we can use C’s bitwise operators to implement the tree-structured global sum. In order to see how this works, it helps to write down the binary (base 2) representation of each of the core ranks, and note the pairings during each stage
+Semelhante ao questão 3 sendo a diferença utilizar o bitwise << para dividir +o tamanho atual da função recursiva:
+std::vector<Node *> create_new_tree_bitwise(std::vector<Node *> nodes)
+{
+
+ auto size = nodes.size();
+
+ if (size >> 1 == 1) // alteração.
+ {
+ auto left = nodes[0];
+ auto right = nodes[1];
+ receive_value(left, right);
+ return {left};
+ }
+
+ auto new_nodes = std::vector<Node *>{};
+
+ for (auto i = 0; i < size; i += 2)
+ {
+ auto left = nodes[i];
+ auto right = nodes[i + 1];
+ receive_value(left, right); // Left receive value from right
+ new_nodes.push_back(left);
+ }
+
+ return create_new_tree_bitwise(new_nodes);
+}
+
What happens if your pseudo-code in Exercise 1.3 or Exercise 1.4 is run when the number of cores is not a power of two (e.g., 3, 5, 6, 7) ? Can you modify the +pseudo-code so that it will work correctly regardless of the number of cores ?
+Se por exemplo, o número de cores, ou Nós for 3 por exemplo, existirá nós que serão "esquecidos" no algoritmo, por tanto +o algoritmo não funcionará corretamente. +
auto size = nodes.size();// size = 3
+
+if (size >> 1 == 1) // alteração.
+{
+ auto left = nodes[0];
+ auto right = nodes[1];
+ // node[2] foi esquecido
+ receive_value(left, right); // Left receive value from right
+ return {left};
+}
+
ou se o por exemplo o número de nós for 7, a última iteração do laço for, os indices i, i+1, serão respectivamente +6 e 7, ou seja será acessado um endereço inválido 7, uma vez que os indices vão de 0 até 6. +
auto new_nodes = std::vector<Node *>{};
+
+ for (auto i = 0; i < size; i += 2)
+ {
+ auto left = nodes[i];
+ auto right = nodes[i + 1];
+ receive_value(left, right); // Left receive value from right
+ new_nodes.push_back(left);
+ }
+
+ return create_new_tree_bitwise(new_nodes);
+
Para isso foram feitas as seguintes modificações:
+ - adicionado condicionamento para verificar se o tamanho é igual 3
+ - alterado o bitwise para apenas um comparador size ==2
+ - verificado se o tamanho dos nós é par, caso nãos seja adicionado uma logica extra.
std::vector<Node *> create_new_tree_bitwise(std::vector<Node *> nodes)
+{
+
+ auto size = nodes.size();
+
+ if (size == 2)
+ {
+ auto left = nodes[0];
+ auto right = nodes[1];
+ receive_value(left, right);
+ return {left}; // Construtor C++ Moderno.
+ }
+ if (size == 3)
+ {
+ auto left = nodes[0];
+ auto middle = nodes[1];
+ auto right = nodes[2];
+ receive_value(left, middle); // Left receive value from middle
+ receive_value(left, right); // Left receive value from right
+ return {left}; // Construtor C++ Moderno.
+ }
+
+ auto new_nodes = std::vector<Node *>{};
+
+ if (size % 2 != 0) // lógica extra.
+ {
+ size = size - 1;
+ new_nodes.push_back(nodes[size]);
+ }
+
+ for (auto i = 0; i < size; i += 2)
+ {
+ auto left = nodes[i];
+ auto right = nodes[i + 1];
+ receive_value(left, right); // Left receive value from right
+ new_nodes.push_back(left);
+ }
+
+ return create_new_tree_bitwise(new_nodes);
+}
+
Além de adicionar uma verificação para saber se o tamanho é par, +foi adicionado dois comandos extras, o primeiro é alterar o tamanho (size), para um valor menor, uma vez que estávamos acessando um índice +maior que o permitido. Segundo foi adicionar o nó que não será percorrido +pelo laço para o vetor new_nodes que será a entrada da próxima função recursiva
+ if (size % 2 != 0) // verificação se é par.
+ {
+ size = size - 1; //1
+ new_nodes.push_back(nodes[size]); // 2
+ }
+
Percebemos que além do 2/2 == 1, a divisão inteira de 3/2 também é igual 1. Por tanto além do caso base de quando o tamanho do vetor de nós ser igual a 2, temos que tratar também quando o número de nós ser igual a 3.
+ if (size == 2)
+ {
+ auto left = nodes[0];
+ auto right = nodes[1];
+ receive_value(left, right);
+ return {left}; // Construtor C++ Moderno.
+ }
+ if (size == 3)
+ {
+ auto left = nodes[0];
+ auto middle = nodes[1];
+ auto right = nodes[2];
+ receive_value(left, middle); // Left receive value from middle
+ receive_value(left, right); // Left receive value from right
+ return {left}; // Construtor C++ Moderno.
+ }
+
Como no exemplo abaixo, onde a segunda iteração do algoritmo o número de nós é 3.
+ +Derive formulas for the number of receives and additions that core 0 carries out using:
+ a. the original pseudo-code for a global sum
+ b. the tree-structured global sum.
+Make a table showing the numbers of receives and additions carried out by core
+0 when the two sums are used with 2, 4, 8, . . . , 1024 cores.
Cores | +Naive | +Tree | +
---|---|---|
2 | +1 | +1 | +
4 | +3 | +2 | +
8 | +7 | +3 | +
16 | +15 | +4 | +
32 | +31 | +5 | +
64 | +63 | +6 | +
128 | +127 | +7 | +
256 | +255 | +8 | +
512 | +512 | +9 | +
1024 | +1023 | +10 | +
Podemos observar claramente que a abordagem ingênua segue a formula, +p -1 e quando usamos a árvore, percebemos que a cada 2 núcleos, +o número de ligações amentar em 1, ou seja, log(p) de base 2. +Podemos ver o número de ligações crescendo linearmente com cada dois núcleos na imagem abaixo +
+Write an essay describing a research problem in your major that would benefit from the use of parallel computing. Provide a rough outline of how parallelism would be used. Would you use task- or data-parallelism ?
+Em contexto de aplicações web, especificamente a camada front-end, vem se popularizando frameworks que utilizam o WebAssembly (Wasm). O WebAssembly é um novo tipo de código que pode ser executado em browsers modernos — se trata de uma linguagem de baixo nível como assembly, com um formato binário compacto que executa com performance quase nativa e que fornece um novo alvo de compilação para linguagens como C/C++, para que possam ser executadas na web developer.mozilla.org. Através do Wasm é possível +implementar algoritmos de Tiny machine learning (TinyML) para classificação, analise de dados sem a necessidade de comunicação com backend. TinyML é amplamente definido como uma rápida e crescente área de aprendizado de máquina e suas aplicações que inclui hardware, algoritmos e software capazes de performar analise de dados em dispositivos de baixo consumo de energia, tipicamente em milliwatts, assim habilitado variados casos de uso e dispositivos que possuem bateria. Em um navegador, ou durante a navegação o usuário está a +todo momento produzindo dados que muitas vezes estão sendo enviados +de forma bruta ou quase bruta para a camada de aplicação ou back-end, onde nela é gasto processamento e memória para a primeira etapa de classificação ou analise dos dados. Uma vez analisado desempenho de técnicas e algoritmos de TinyML utilizando WebAssembly, pode ser possível transferir a responsabilidade da analise dos dados para o front-end. Em contexto de navegador quase tudo é paralelo ou distribuído, +uma aba ou tab em inglês é um processo diferente. Criar um uma extensão, que faça aquisição e analise dos dados de diferentes abas, seria criar um sistema que se comunica com diferentes processos por meio de mensagens e o algoritmo de aprendizado pode fazer uso de idealmente uma ou duas threads para realizar a analise rapidamente. Por tanto é um +sistema que é task-and-data parallel.
+Capítulo 2: 1-3, 5, 7, 10, 15-17, 19-21, 24; (13 questões)
+When we were discussing floating point addition, we made the simplifying assumption that each of the functional units took the same amount of time. Suppose that fetch and store each take 2 nanoseconds and the remaining operations each take 1 nanosecond.
+How long does a floating point addition take with these assumptions ?
+How long will an unpipelined addition of 1000 pairs of floats take with these assumptions ?
+How long will a pipelined addition of 1000 pairs of floats take with these assumptions ?
+The time required for fetch and store may vary considerably if the operands/results are stored in different levels of the memory hierarchy. Suppose that a fetch from a level 1 cache takes two nanoseconds, while a fetch from a level 2 cache takes five nanoseconds, and a fetch from main memory takes fifty nanoseconds. What happens to the pipeline when there is a level 1 cache miss on a fetch of one of the operands? What happens when there is a level 2 miss ?
+ +Explain how a queue, implemented in hardware in the CPU, could be used to improve the performance of a write-through cache.
+ +Recall the example involving cache reads of a two-dimensional array (page 22). How does a larger matrix and a larger cache affect the performance of the two pairs of nested loops? What happens if MAX = 8 and the cache can store four lines ? How many misses occur in the reads of A in the first pair of nested loops ? How many misses occur in the second pair ?
+ +Does the addition of cache and virtual memory to a von Neumann system change its designation as an SISD system ? What about the addition of +pipelining? Multiple issue? Hardware multithreading ?
+ +Discuss the differences in how a GPU and a vector processor might execute the following code:
+
Suppose a program must execute 10¹² instructions in order to solve a particular problem. Suppose further that a single processor system can solve the problem in 10⁶ seconds (about 11.6 days). So, on average, the single processor system executes 10⁶ or a million instructions per second. Now suppose that the program has been parallelized for execution on a distributed-memory system. Suppose also that if the parallel program uses p processors, each processor will execute 10¹² /p instructions and each processor must send 10⁹ ( p − 1) messages. Finally, suppose that there is no additional overhead in executing the +parallel program. That is, the program will complete after each processor has executed all of its instructions and sent all of its messages, and there won’t be any delays due to things such as waiting for messages.
+Suppose it takes 10⁻⁹ seconds to send a message. How long will it take the program to run with 1000 processors, if each processor is as fast as the single processor on which the serial program was run ?
+Suppose it takes 10⁻³ seconds to send a message. How long will it take the program to run with 1000 processors ? + Resposta questão 10
+Suppose a shared-memory system uses snooping cache coherence and +write-back caches. Also suppose that core 0 has the variable x in its cache, and it executes the assignment x = 5. Finally suppose that core 1 doesn’t have x in its cache, and after core 0’s update to x, core 1 tries to execute y = x. What value will be assigned to y ? Why ?
+Suppose that the shared-memory system in the previous part uses a +directory-based protocol. What value will be assigned to y ? Why ?
+A parallel program that obtains a speedup greater than p—the number of processes or threads—is sometimes said to have superlinear speedup. However, many authors don’t count programs that overcome “resource limitations” as having superlinear speedup. For example, a program that must use secondary storage for its data when it’s run on a single processor system might be able to fit all its data into main memory when run on a large distributed-memory system. Give another example of how a program might overcome a resource limitation and obtain speedups greater than p
+ +Suppose
Is a program that obtains linear speedup strongly scalable ? Explain your answer.
+ +Bob has a program that he wants to time with two sets of data, input_data1 and input_data2. To get some idea of what to expect before adding timing functions to the code he’s interested in, he runs the program with two sets of data and the Unix shell command time: +
+ The timer function Bob is using has millisecond resolution. Should Bob use it to time his program with the first set of data ? What about the second set of data ? Why or why not ? + +When we were discussing floating point addition, we made the simplifying assumption that each of the functional units took the same amount of time. Suppose that fetch and store each take 2 nanoseconds and the remaining operations each take 1 nanosecond.
+ a. How long does a floating point addition take with these assumptions ?
b. How long will an unpipelined addition of 1000 pairs of floats take with these assumptions ?
+c. How long will a pipelined addition of 1000 pairs of floats take with these assumptions ?
+d. The time required for fetch and store may vary considerably if the operands/results are stored in different levels of the memory hierarchy. Suppose that a fetch from a level 1 cache takes two nanoseconds, while a fetch from a level 2 cache takes five nanoseconds, and a fetch from main memory takes fifty nanoseconds. What happens to the pipeline when there is a level 1 cache miss on a fetch of one of the operands? What happens when there is a level 2 miss ?
+Instructions | +Time in nanosecond | +
---|---|
Fetch | +2 | +
Store | +2 | +
Functional OP | +1 | +
"As an alternative, suppose we divide our floating point adder into seven separate pieces of hardware or functional units. The first unit will fetch two operands, +the second will compare exponents, and so on." (Página 26)
+O Author do livro considera que existe sete operações, considerando que duas delas são fetch e store custa 2 nanosegundos e o restante 1 nanosegundo.
+ 1*5 +2*2 = 9 nanosegundos
+
Considerando que exitem 1000 pares de valores vão serem somados:
+ 1000*9 = 9000 nanosegundos
+
foi pensado o seguinte: Nó memento que o dado passa pelo fetch, e vai para a próxima operação já +é realizado o fetch da segunda operação. Executando o pipeline:
+Tempo em nanosegundos | +Fetch | +OP1 | +OP2 | +OP3 | +OP4 | +OP5 | +Store | +
---|---|---|---|---|---|---|---|
0 | +1 | +wait | +wait | +wait | +wait | +wait | +wait | +
2 | +2 | +1 | +wait | +wait | +wait | +wait | +wait | +
3 | +2 | +wait | +1 | +wait | +wait | +wait | +wait | +
4 | +3 | +2 | +wait | +1 | +wait | +wait | +wait | +
5 | +3 | +wait | +2 | +wait | +1 | +wait | +wait | +
6 | +4 | +3 | +wait | +2 | +wait | +1 | +wait | +
7 | +4 | +wait | +3 | +wait | +2 | +wait | +1 | +
8 | +5 | +4 | +wait | +3 | +wait | +2 | +wait | +
9 | +5 | +wait | +4 | +wait | +3 | +wait | +2 | +
10 | +6 | +5 | +wait | +4 | +wait | +3 | +2 | +
11 | +6 | +wait | +5 | +wait | +4 | +wait | +3 | +
Percebe-se que a primeira instrução irá ser finalizada ou sumir na tabela quanto for 9 segundos +ou seja,a primeira instrução dura 9 segundos, no entanto, no momento em que a primeira instrução +é finalizada,a segunda já começa a ser finalizada ou seja, demora apenas 2 nanosegundos até segunda operação ser finalizada e mais 2 nanosegundos para a terceira ser finalizada e assim por diante. Por tanto para executar todos os 1000 dados, o custo total fica:
+ 9 + 999*2 = 2007
+
No caso, considerando que a cache nível não falhe a tabela continua mesma, +pois o fetch e store custam o mesmo 2 nanosegundos:
+Tempo em nanosegundos | +Fetch | +OP1 | +OP2 | +OP3 | +OP4 | +OP5 | +Store | +
---|---|---|---|---|---|---|---|
10 | +6 | +5 | +wait | +4 | +wait | +3 | +2 | +
11 | +6 | +wait | +5 | +wait | +4 | +wait | +3 | +
mas se imaginarmos que na 12 iteração o Fetch e Store passa a custar 5 nanosegundos:
+Tempo em nanosegundos | +Fetch | +OP1 | +OP2 | +OP3 | +OP4 | +OP5 | +Store | +
---|---|---|---|---|---|---|---|
10 | +6 | +5 | +wait | +4 | +wait | +3 | +2 | +
11 | +6 | +wait | +5 | +wait | +4 | +wait | +3 | +
12 | +6 | +wait | +wait | +5 | +wait | +4 | +3 | +
13 | +6 | +wait | +wait | +wait | +5 | +4 | +3 | +
14 | +6 | +wait | +wait | +wait | +5 | +4 | +3 | +
15 | +7 | +6 | +wait | +wait | +5 | +4 | +3 | +
16 | +7 | +wait | +6 | +wait | +wait | +5 | +4 | +
Quando mais lento fica a transferência para a memória principal, mais nítido fica o gargalo de Von Neumann, ou seja, percebe-se que a performance do processador fica limitado a taxa de transferência de dados com a memória principal.
+Explain how a queue, implemented in hardware in the CPU, could be used to improve the performance of a write-through cache.
+Como observado na questão 1 cada momento que a escrita é debilitada, fica nítido o gargalo de Von Neuman se considerarmos que uma escrita na cache é uma escrita na memória principal, então cada Store iria demorar 50 nano segundos. Colocando uma fila e supondo que ela nunca fique cheia, a CPU não irá gastar tanto tempo no Store, mas uma vez a fila +cheia, a CPU terá que aguardar uma escrita na memória principal.
+Tempo em nanosegundos | +Fetch | +OP1 | +OP2 | +OP3 | +OP4 | +OP5 | +Store | +
---|---|---|---|---|---|---|---|
10 | +6 | +5 | +wait | +4 | +wait | +3 | +2 | +
11 | +6 | +wait | +5 | +wait | +4 | +wait | +3 | +
12 | +6 | +wait | +wait | +5 | +wait | +4 | +3 | +
13 | +6 | +wait | +wait | +wait | +5 | +4 | +3 | +
14 | +6 | +wait | +wait | +wait | +5 | +4 | +3 | +
15 | +7 | +6 | +wait | +wait | +5 | +4 | +3 | +
16 | +7 | +wait | +6 | +wait | +wait | +5 | +4 | +
Recall the example involving cache reads of a two-dimensional array (page 22). How does a larger matrix and a larger cache affect the performance of the two pairs of nested loops? What happens if MAX = 8 and the cache can store four lines ? How many misses occur in the reads of A in the first pair of nested loops ? How many misses occur in the second pair ?
+double A[MAX][MAX], x[MAX], y[MAX];
+
+//. . .
+// Initialize A and x, assign y = 0 ∗/
+//. . .
+
+
+/∗ First pair of loops ∗/
+for (i = 0; i < MAX; i++)
+ for (j = 0; j < MAX; j++)
+ y[i] += A[i][j]∗x[j];
+
+//. . .
+// Assign y = 0 ∗/
+//. . .
+
+/∗ Second pair of loops ∗/
+for (j = 0; j < MAX; j++)
+ for (i = 0; i < MAX; i++)
+ y[i] += A[i][j]∗x[j];
+
Cache line | +Elements of A | +
---|---|
0 | +A[0][0] A[0][1] A[0][2] A[0][3] | +
1 | +A[1][0] A[1][1] A[1][2] A[1][3] | +
2 | +A[2][0] A[2][1] A[2][2] A[2][3] | +
3 | +A[3][0] A[3][1] A[3][2] A[3][3] | +
Supondo que a cache tenha a mesma proporção do que a Matrix, o número de cache miss seria igual ao número de linhas da matriz, como apontado no exemplo dado no livro, quando o processador Pede o valor A[0][0], baseado na ideia de vizinhança, a cache carrega todas as outras colunas da linha 0, portanto é plausível pensar que o número de miss é igual ao número de linhas, ou seja, +o número miss é igual a MAX, pois a cache tem o mesmo número de linhas que a matrix A, suponto que não preciso me preocupar com x e y.
+Tendo a a cache armazenando metade dos valores de uma linha da Matriz A, então +para cada linha da Matriz, vai haver duas cache miss, a primeira np A[i][0] e a segunda no A[i][4]. Outro ponto é que como a cache só possui 4 linhas, então +após ocorrer os cache misses A[0][0] ,A[0][4] e A[1][0], A[1][4] toda a cache +terá sindo preenchida, ou seja, Tento a matriz 8 linhas e para cada linha tem 2 cache miss por tanto:
+ 8*2 =16 cache miss
+
como tanto a primeira parte quando na segunda parte, percorre-se todas as linhas +irá haver 16 cache miss, suponto que não preciso me preocupar com x e y.
+Cache line | +Elements of A | +
---|---|
0 | +A[0][0] A[0][1] A[0][2] A[0][3] | +
1 | +A[0][4] A[0][5] A[0][6] A[0][7] | +
2 | +A[1][0] A[1][1] A[1][2] A[1][3] | +
3 | +A[1][4] A[1][5] A[1][6] A[1][7] | +
No segundo par de loops, vemos que o segundo laço for, percorre os valores: +A[0][0], A[1][0], A[2][0] ... A[7][0], para quando j =0. Isso faz com que +todo hit seja miss, ou seja iremos ter miss para cada acesso em A, portanto:
+ 8*8 = 64 cache miss
+
Does the addition of cache and virtual memory to a von Neumann system change its designation as an SISD system ? What about the addition of +pipelining? Multiple issue? Hardware multithreading ?
+Um SISD system ou Single Instruction Single Data system, são sistemas que executam uma única instrução por vez e sua taxa de transferência de dados é +de um item por vez também.
+Adicionar um cache e memória virtual, pode ajudar a reduzir o tempo que única instrução é lida da memória principal, mas não aumenta o número de +instruções buscadas na operação Fetch ou na Operação Store, por tanto o sistema +continua sendo Single Instruction Single Data.
+Como demostrado na questão 1, ao adicionar um pipeline, podemos realizar a mesma instrução complexa em múltiplos dados, +ou seja, Single Instruction Multiple Data System, portanto sim.
+No momento em que possibilitamos uma máquina executar antecipadamente uma instrução ou possibilitamos a execução de múltiplas threads, nesse momento então a máquina está executando várias instruções em vários dados ao mesmo tempo, por tanto o sistema se torna Multiple Instruction Multiple Data system, ou seja, a designação muda.
+Discuss the differences in how a GPU and a vector processor might execute the following code: +
+Um processo de vetorização em cima desse laço for dividiria as entradas +em chucks ou blocos de dados e executaria em paralelo a instrução complexa. +Algo como:
+//executando o bloco paralelamente.
+y[0] += a∗x[0];
+y[1] += a∗x[1];
+y[2] += a∗x[2];
+y[3] += a∗x[3];
+
+z[0]∗z[0] // executando em paralelo
+z[1]∗z[1] // executando em paralelo
+z[2]∗z[2] // executando em paralelo
+z[3]∗z[3] // executando em paralelo
+// somando tudo depois
+sum+= z[0]∗z[0] + z[1]∗z[1] + z[2]∗z[2] + z[3]∗z[3];
+
Atualmente essa operação em GPU é muito mais interessante, pois hoje podemos +compilar ou gerar instruções complexas que podem ser executas em GPU. A primeira vantagem seria separar o calculo do sum:
+ sum += z[i]∗z[i];
+
do calculo do y
+ y[i] += a∗x[i];
+
ficando assim:
+# version 330
+
+layout(location = 0) in vec4 x;
+layout(location = 1) in mat4 a;
+layout(location = 2) in vec4 y;
+
+/* o buffer gl_Position é ré-inserido no y */
+void main()
+{
+ gl_Position = a*x + y;
+}
+//
+
# version 330
+
+layout(location = 0) in mat4 z;
+uniform float sum;
+
+/* transpose é uma função que calcula a transposta já existem no Glsl */
+void main()
+{
+ mat4 temp = transpose(z) * z;
+
+ sum = 0;
+ for (int i = 0; i < 4; i++)
+ // desde que o laço for seja baseado em constantes ou variáveis uniforms
+ // esse laço for é possível.
+ {
+ sum += temp[i];
+ }
+
+ // recupera o valor no index 0
+ gl_Position = vec4(sum, 0.0, 0.0, 0.0, 0.0);
+}
+
Suppose a program must execute
Suppose it takes
Suppose it takes
import datetime
+
+NUMBER_OF_INSTRUCTIONS = 10**12
+NUMBER_OF_MESSAGES = 10**9
+AVERANGE_SECOND_PER_INSTRUCTIONS = (10**6) / NUMBER_OF_INSTRUCTIONS
+
+
+def cost_time_per_instruction(instructions: int) -> float:
+ return AVERANGE_SECOND_PER_INSTRUCTIONS * instructions
+
+
+def number_of_instructions_per_processor(p: int) -> int:
+ return NUMBER_OF_INSTRUCTIONS/p
+
+
+def number_of_messagens_per_processor(p: int) -> int:
+ return NUMBER_OF_MESSAGES * (p-1)
+
+
+def simulate(time_per_message_in_seconds: float, processors: int):
+ print(
+ f'time to send a message: {time_per_message_in_seconds} processors: {processors}')
+ instructions = number_of_instructions_per_processor(processors)
+ number_of_messages = number_of_messagens_per_processor(processors)
+ each_process_cost_in_seconds = cost_time_per_instruction(instructions)
+
+ total_messages_in_seconds = time_per_message_in_seconds * number_of_messages
+
+ result = total_messages_in_seconds + each_process_cost_in_seconds
+ result_date = datetime.timedelta(seconds=result)
+
+ print(f'executing instructions is {instructions}')
+ print(f'spend sending messages is {total_messages_in_seconds}')
+
+ print(f'total time in seconds: {result}')
+ print(f'total time in HH:MM:SS {result_date}')
+
+
+def a():
+
+ time_per_message_in_seconds = 1e-9
+ processors = 1e3
+ simulate(time_per_message_in_seconds, processors)
+
+
+def b():
+ time_per_message_in_seconds = 1e-3
+ processors = 1e3
+ simulate(time_per_message_in_seconds, processors)
+
+
+def main():
+ print('A:')
+ a()
+ print('B:')
+ b()
+
+
+if __name__ == '__main__':
+ main()
+
python chapter_2/question_10/main.py
+A:
+time to send a message: 1e-09 processors: 1000.0
+executing instructions is 1000000000.0
+spend sending messages is 999.0000000000001
+total time in seconds: 1999.0
+total time in HH:MM:SS 0:33:19
+B:
+time to send a message: 0.001 processors: 1000.0
+executing instructions is 1000000000.0
+spend sending messages is 999000000.0
+total time in seconds: 999001000.0
+total time in HH:MM:SS 11562 days, 12:16:40
+
Suppose a shared-memory system uses snooping cache coherence and +write-back caches. Also suppose that core 0 has the variable x in its cache, and it executes the assignment x = 5. Finally suppose that core 1 doesn’t have x in its cache, and after core 0’s update to x, core 1 tries to execute y = x. What value will be assigned to y ? Why ?
+Suppose that the shared-memory system in the previous part uses a +directory-based protocol. What value will be assigned to y ? Why ?
+Can you suggest how any problems you found in the first two parts might be solved ?
+Não é possível determinar qual valor será atribuído ao y independentemente se for write-back ou write-through, uma vez que não +houve uma sincronização entre os cores sobre o valor de x. A atribuição +de y = x do core 1 pode ocorrer antes ou depois das operações no core 0.
+Com o sistema de arquivos, ao core 0 irá notificar o a memória principal que a consistência dos dados foi comprometida, no entanto, ainda não dá +para saber qual o valor de y, uma vez que a atribuição de y = x do core 1 pode ocorrer antes ou depois das operações no core 0.
+Existe dois problemas, o problema da consistência do dados, temos que garantir que ambos os cores façam alterações que ambas sejam capaz de +ler e o segundo é um mecanismo de sincronização, onde por exemplo, o core 1 espera o core 0 finalizar o seu processamento com a variável x para ai sim começar o seu. Podemos utilizar por exemplo um mutex, onde +inicialmente o core 0 faria o lock e ao finalizar ele entrega a chave a qual, o core 1 pegaria.
+a. Suppose the run-time of a serial program is given by
b. Suppose that
Podemos observar a Amadahl's law, "a lei de Amadahl diz: a menos que virtualmente todo o programa serial seja paralelizado, o possível speedup, ou ganhado de performance será muito limitado, independentemente dos números de cores disponíveis." Nó exemplo em questão, podemos observar que a partir de +32 cores, o tempo da aplicação fica estagnado por volta dos 1606 micro segundos.
+Mesmo com da lei de Amdahl, podemos observar que o aumento de performance é bastante significante, valendo a pena paralelizar a aplicação, também pelo fato que os hardwares atualmente possuem mais +de um core.
+Sabendo que o tempo serial é o quadrático da entrada, considerei o
+tempo de overhead sendo:
Sabendo que o tempo serial é o quadrático da entrada, considerei o
+tempo de overhead sendo:
Para essa atividade foi utilizado Python na sua versão 3.10.4 +para instalar as dependências do script e criar os gráficos:
+ +A parallel program that obtains a speedup greater than p—the number of processes or threads—is sometimes said to have superlinear speedup. However, many authors don’t count programs that overcome “resource limitations” as having superlinear speedup. For example, a program that must use secondary storage for its data when it’s run on a single processor system might be able to fit all its data into main memory when run on a large distributed-memory system. Give another example of how a program might overcome a resource limitation and obtain speedups greater than p
+Sistemas embarcados com CPUs multi-core. Atualmente já existem microcontroladores como ESP32 que dependendo do modelo pode possuir +mais de um núcleo de processamento. Sabendo que todo o programa fica carregado na memória, então uma aplicação como um servidor HTTP, pode +ter mais que o dobro de performance, quando observado o seu requests por segundo.
+Fazemos o seguinte cenário:
+Temos um desenvolvedor que sabe que executar operações de escrita em hardware é uma operação insegura e utiliza estruturas de +dados para sincronização dessas operações, cada dispositivo tem seu tempo de sincronização. Temos que o dispositivo A custa 5 milissegundos, +e dispositivo B custa 4 milissegundos. Sabendo que se criarmos a aplicação em single core, temos que esperar a sincronização de A e B e que +modificar o tempo de sincronização de um dispositivo custa 3 milissegundos. Dado que se tem 2 cores, 2 conversores AD, se delegarmos cada dispositivo +para um core, eliminaremos 3 milissegundos do processo de escrita no seu pior cenário. Supondo que o tempo de um request fique 30% na escrita de +um dispositivo e que ele gasta em outras operações 8 milissegundos temos dois cenários,
+Suppose
Se considerarmos a constante
Dado a definição do autor sim. Para o autor escalável é quando a eficiência de um programa paralelo se mantém constante, ou seja, se existe uma taxa que relaciona o crescimento do tamanho do problema com o crescimento do número de threads/processos, então o programa paralelo é fracamente escalável (weakly scalable), .
+Is a program that obtains linear speedup strongly scalable ? Explain your answer.
+Dado a definição do autor sim. Para o autor escalável é quando a eficiência de um programa paralelo se mantém constante. Linear speedup +pode ser escrito pela seguinte expressão:
+Portanto dado que eficiência é dado pela seguinte expressão:
+se o speedup for linear, ou seja,
Bob has a program that he wants to time with two sets of data, input_data1 and input_data2. To get some idea of what to expect before adding timing functions to the code he’s interested in, he runs the program with two sets of data and the Unix shell command time: +
$ time ./bobs prog < input data1
+real 0m0.001s
+user 0m0.001s
+sys 0m0.000s
+$ time ./bobs prog < input data2
+real 1m1.234s
+user 1m0.001s
+sys 0m0.111s
+
Segundo a referência o commando time, retorna três valores:
+Na primeira chamada observamos que o tempo coletado é praticamente 0, ou seja, o tempo levado para executar o programa esta fora da resolução do relógio do sistema, por tanto não podemos concluir nada sobre a primeira chamada e se essa for a primeira chamada é bem provável que a próxima também dê praticamente 0, uma vez que a aplicação pode ter pouco tamanho +de entrada se comparado a máquina em questão.
+Já no segundo timer podemos observar informações sobre a aplicação, estão dentro da resolução do relógio e que maior parte da aplicação foi gasta em em user mode. Bob pode fazer proveito dessas informações.
+If you haven’t already done so in Chapter 1, try to write pseudo-code for our tree-structured global sum, which sums the elements of loc_bin_cts. First consider how this might be done in a shared-memory setting. Then consider how this might be done in a distributed-memory setting. In the shared-memory setting, which variables are shared and which are private ?
+Um código em C++, mas sem o uso de estruturas de dados de programação paralela, pode ser observado na resposta da questão 5 Question_5.cpp. Também foi implementado o mesmo algoritmo em rust e nessa implementação foi utilizado threads, smart pointers e mutex para +resolver o problema. O código pode ser observado aqui: main.rs
+use std::{
+ sync::{Arc, Mutex},
+ thread::JoinHandle,
+};
+
+#[derive(Debug)]
+struct Node {
+ data: Vec<i32>,
+ neighborhoods: Mutex<Vec<JoinHandle<i32>>>,
+}
+
+impl Node {
+ fn from_value(data: i32) -> Node {
+ Node {
+ data: vec![data],
+ neighborhoods: Mutex::new(vec![]),
+ }
+ }
+
+ fn compute(self) -> i32 {
+ /*
+ Em termos de memória essa função desaloca toda memória
+ usada pela estrutura Node e retorna um inteiro de 32bits.
+ Dado que foi utilizado safe rust e o código compila, logo esse
+ código está livre data race e como não referências cíclicas
+ também está livre de memory leak.
+ */
+ let result: i32 = self.data.iter().sum();
+
+ let neighborhoods = self.neighborhoods.into_inner().unwrap();
+
+ let neighborhoods_sum: i32 = neighborhoods
+ .into_iter()
+ .map(|handle| handle.join().expect("Unable to lock neighborhood"))
+ .sum();
+
+ result + neighborhoods_sum
+ }
+}
+
+fn start_to_compute_node(node: Node) -> JoinHandle<i32> {
+ std::thread::spawn(move || {
+ let result = node.compute();
+ std::thread::sleep(std::time::Duration::from_micros(500));
+
+ result
+ })
+}
+
+fn receive_value(left: Arc<Node>, right: Arc<Node>) {
+ let right = Arc::try_unwrap(right).unwrap();
+
+ let mut left_neighborhoods = left
+ .neighborhoods
+ .lock()
+ .expect("Unable to lock neighborhood");
+
+ left_neighborhoods.push(start_to_compute_node(right))
+}
+
+fn create_new_tree_bitwise(mut nodes: Vec<Arc<Node>>) -> Vec<Arc<Node>> {
+ let size = nodes.len();
+
+ match size {
+ 2 => {
+ let left = nodes.remove(0);
+ let right = nodes.remove(0);
+
+ receive_value(left.clone(), right);
+
+ vec![left]
+ }
+
+ 3 => {
+ let left = nodes.remove(0);
+ let middle = nodes.remove(0);
+ let right = nodes.remove(0);
+
+ receive_value(left.clone(), middle);
+ receive_value(left.clone(), right);
+
+ vec![left]
+ }
+
+ _ => {
+ let mut new_nodes = vec![];
+
+ let mut size = size;
+
+ if size % 2 != 0 {
+ size = size - 1;
+
+ new_nodes.push(nodes.remove(size - 1));
+ }
+
+ for i in (0..size).step_by(2) {
+ let left = nodes.remove(0);
+ let right = nodes.remove(0);
+ println!("i: {} left: {:?} right: {:?}", i, left, right);
+ receive_value(left.clone(), right);
+ new_nodes.push(left);
+ }
+ println!("Next iteration");
+
+ create_new_tree_bitwise(new_nodes)
+ }
+ }
+}
+
+fn main() {
+ let data = vec![8, 19, 7, 15, 7, 13, 12, 14];
+
+ let nodes: Vec<Arc<Node>> = data
+ .clone()
+ .iter()
+ .map(|&v| Node::from_value(v))
+ .map(|node| Arc::new(node))
+ .collect();
+
+ let root = create_new_tree_bitwise(nodes)[0].clone();
+
+ let root = Arc::try_unwrap(root).unwrap();
+
+ let total = root.compute();
+
+ assert!( total == data.iter().sum::<i32>());
+ println!(
+ "total: {} data sum {} ",
+ total,
+ data.iter().sum::<i32>()
+ );
+}
+
+#[test]
+fn tree_sum() {
+ /*
+ Teste com várias entradas
+ */
+ let data_tests = vec![
+ vec![8, 19, 7, 15, 7, 13, 12, 14],
+ vec![8, 19, 7, 15, 7, 13, 12],
+ vec![8, 19, 7, 15, 7, 13],
+ vec![8, 19, 7, 15, 7],
+ vec![8, 19, 7, 15],
+ vec![8, 19, 7],
+ vec![8, 19],
+ ];
+
+ for data in data_tests {
+ let nodes: Vec<Arc<Node>> = data
+ .clone()
+ .iter()
+ .map(|&v| Node::from_value(v))
+ .map(|node| Arc::new(node))
+ .collect();
+
+ let root = create_new_tree_bitwise(nodes)[0].clone();
+
+ let root = Arc::try_unwrap(root).unwrap();
+
+ assert_eq!(root.compute(), data.iter().sum::<i32>());
+ }
+}
+
caso tenha o rust instalado você pode observar a execução do caso de teste +com o comando cargo.
+ +O teste é mesmo teste feito em c++.
+Capítulo 3: 2, 4, 6, 9, 11, 12, 13, 16, 17, 19, 20, 22, 23, 27 e 28 (16 questões);
+Modify the program that just prints a line of output from each process (mpi_output.c) so that the output is printed in process rank order: process 0s output first, then process 1s, and so on.
+Suppose comm_sz = 4 and suppose that x is a vector with
Finding prefix sums is a generalization of global sum. Rather than simply finding the sum of
+
+the prefix sums are the n partial sums
+
An alternative to a butterfly-structured allreduce is a ring-pass structure. In a ring-pass, if there are
sum = temp val = my val;
+
+for (i = 1; i < p; i++) {
+ MPI Sendrecv replace(&temp val, 1, MPI INT, dest,
+ sendtag, source, recvtag, comm, &status);
+ sum += temp val;
+}
+
MPI Scatter and MPI Gather have the limitation that each process must send or receive the same number of data items. When this is not the case, we must use the MPI functions MPI Gatherv and MPI Scatterv. Look at the man pages for these functions, and modify your vector sum, dot product program so that it can correctly handle the case when n isn’t evenly divisible by +comm_sz.
+Suppose comm_sz
MPI_Type_contiguous can be used to build a derived datatype from a collection of contiguous elements in an array. Its syntax is +
+ Modify the Read_vector and Print_vector functions so that they use an MPI datatype created by a call to MPI_Type_contiguous and a count argument of 1 in the calls to MPI_Scatter and MPI_Gather. +MPI_Type_indexed can be used to build a derived datatype from arbitrary array elements. Its syntax is +
int MPI_Type_indexed(
+ int count, /* in */
+ int array_of_blocklengths[], /* in */,
+ int array_of_displacements[], /* in */,
+ MPI_Datatype old_mpi_t, /* in */
+ MPI_Datatype∗ new_mpi_t_p /* out */);
+
the upper triangular part is the elements 0, 1, 2, 3, 5, 6, 7, 10, 11, 15. Process
+0 should read in an
The functions MPI_Pack and MPI_Unpack provide an alternative to derived datatypes for grouping data. MPI_Pack copies the data to be sent, one block at a time, into a user-provided buffer. The buffer can then be sent and received. After the data is received, MPI_Unpack can be used to unpack it from the receive buffer. The syntax of MPI_Pack is +
int MPI_Pack(
+ void* in_buf, /* in */
+ int in_buf_count, /* in */
+ MPI_Datatype datatype, /* in */
+ void* pack_buf, /* out */
+ int pack_buf_sz, /* in */
+ int* position_p, /* in/out */
+ MPI_Comm comm /* in */);
+
Time our implementation of the trapezoidal rule that uses MPI_Reduce. How will + you choose n, the number of trapezoids ? How do the minimum times compare to the mean and median times ? What are the speedups ? What are the efficiencies ? On the basis of the data you collected, would you say that the trapezoidal rule is scalable ?
+Although we don’t know the internals of the implementation of MPI_Reduce, we might guess that it uses a structure similar to the binary tree we discussed. If this is the case, we would expect that its run-time would grow roughly at the rate of
+
+ for some constants
Find the speedups and efficiencies of the parallel odd-even sort. Does the program obtain linear speedups? Is it scalable ? Is it strongly scalable ? Is it weakly scalable ?
+Modify the parallel odd-even transposition sort so that the Merge functions simply swap array pointers after finding the smallest or largest elements. What effect does this change have on the overall run-time ?
+Quando o compilador não é capaz de vetorizar automaticamente, ou vetoriza de forma ineficiente, o OpenMP provê a diretiva omp simd, com a qual o programador pode indicar um laço explicitamente para o compilador vetorizar. No código abaixo, a inclusão da cláusula reduction funciona de forma similar a flag -ffast-math, indicando que a redução na variável soma é segura e deve ser feita. +
+Por que não é necessário usar a cláusula private(x) neste caso mas seria caso a diretiva omp simd fosse combinada com a diretiva omp parallel for ? +Modify the trapezoidal rule so that it will correctly estimate the integral even if comm_sz doesn’t evenly divide n. (You can still assume that n
#include <mpi.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+
+double trap(double a, double b, long int n);
+
+double function(double x);
+
+int main(int argc, char *argv[])
+{
+
+ int my_rank;
+ int comm_sz;
+
+ int message_tag = 0;
+ // resceber argumentos globais via linha de commando
+ MPI_Init(&argc, &argv);
+
+ const double A = atof(argv[1]);
+ const double B = atof(argv[2]);
+ const int N = atoi(argv[3]);
+
+
+
+ MPI_Comm_rank(MPI_COMM_WORLD, &my_rank);
+ MPI_Comm_size(MPI_COMM_WORLD, &comm_sz);
+
+ double h = (B - A) / N;
+
+ int local_n = N / comm_sz;
+ /*
+ Como podemos observar que quando a divisão
+ inteira N/comm_sz o n local, não corresponde
+ ao número total de trapézios desejados.
+ Ex: 1024/3 == 341, sendo que 341*3 == 1023
+ */
+ double local_a = A + my_rank * local_n * h´;
+ double local_b = local_a + local_n * h;
+ double result_integral = trap(local_a, local_b, local_n);
+
+ if (my_rank != 0)
+ {
+ MPI_Send(&result_integral, 1, MPI_DOUBLE, 0, message_tag, MPI_COMM_WORLD);
+ }
+ else // my_rank ==0
+ {
+
+ for (int source = 1; source < comm_sz; source++)
+ {
+ double result_integral_source;
+ MPI_Recv(&result_integral_source, 1, MPI_DOUBLE, source, message_tag, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
+ result_integral += result_integral_source;
+ }
+ printf("with n = %d trapezoids, our estimate\n", N);
+ printf("of the integral from %f to %f = %.15e\n", A, B, result_integral);
+ }
+
+ MPI_Finalize();
+
+ return 0;
+}
+
+double trap(double a, double b, long int n)
+{
+
+ double h = (b - a) / n;
+
+ double approx = (function(a) + function(b)) / 2.0;
+ for (int i = 1; i < n - 1; i++)
+ {
+ // printf("f_%i \n", i);
+ double x_i = a + i * h;
+
+ approx += function(x_i);
+ }
+
+ return h * approx;
+}
+
+double function(double x) { return x * x; }
+
Sabendo que pode ocorrer divisões cujo o número não pertence aos números racionais, por tanto se faz necessário utilizar a seguinte
+expressão:
+
struct range
+{
+ int first;
+ int last;
+};
+
+struct range new_range_2(int thread_index, int p, int n)
+{
+ struct range r;
+
+ int division = n / p;
+ int rest = n % p;
+
+ if (thread_index < rest)
+ {
+ r.first = thread_index * (division + 1);
+ r.last = r.first + division + 1;
+ }
+ else
+ {
+ r.first = thread_index * division + rest;
+ r.last = r.first + division;
+ }
+
+ return r;
+}
+...
+
+int main(int argc, char *argv[])
+{
+
+ ...
+ /*
+ onde thread_index é o rank,
+ o número de cores é o número de processos,
+ o tamanho do vetor é o número de trapézios
+ */
+ struct range r = new_range_2(my_rank,comm_sz,N);
+
+ double h = (B - A) / N;
+
+ /*
+ perceba que o número local de trapézios é
+ o tamanho do intervalo calculado
+ */
+ int local_n = r.last -r.first;
+ double local_a = A + r.first * h;
+ double local_b = A + r.last * h;
+ double result_integral = trap(local_a, local_b, local_n);
+
+ printf("local n: %i local a: %f local b %f \n", local_n, local_a, local_b);
+}
+
mpicc trap_rule.c -o trap; mpiexec -n 3 ./trap 1 3 1024
+local n: 342 local a: 1.000000 local b 1.667969
+local n: 341 local a: 1.667969 local b 2.333984
+local n: 341 local a: 2.333984 local b 3.000000
+with n = 1024 trapezoids, our estimate
+of the integral from 1.000000 to 3.000000 = 8.633069768548012e+00
+
mpicc trap_rule.c -o trap; mpiexec -n 4 ./trap 1 3 2024
+local n: 506 local a: 1.000000 local b 1.500000
+local n: 506 local a: 1.500000 local b 2.000000
+local n: 506 local a: 2.000000 local b 2.500000
+local n: 506 local a: 2.500000 local b 3.000000
+with n = 2024 trapezoids, our estimate
+of the integral from 1.000000 to 3.000000 = 8.645439504647232e+00
+
mpicc trap_rule.c -o trap; mpiexec -n 3 ./trap 0 3 1024
+local n: 342 local a: 0.000000 local b 1.001953
+local n: 341 local a: 1.001953 local b 2.000977
+local n: 341 local a: 2.000977 local b 3.000000
+with n = 1024 trapezoids, our estimate
+of the integral from 0.000000 to 3.000000 = 8.959068736061454e+00
+
#include <mpi.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+struct range
+{
+ int first;
+ int last;
+};
+
+struct range new_range_2(int thread_index, int p, int n)
+{
+ struct range r;
+
+ int division = n / p;
+ int rest = n % p;
+
+ if (thread_index < rest)
+ {
+ r.first = thread_index * (division + 1);
+ r.last = r.first + division + 1;
+ }
+ else
+ {
+ r.first = thread_index * division + rest;
+ r.last = r.first + division;
+ }
+
+ return r;
+}
+``
+double trap(double a, double b, long int n);
+
+double function(double x);
+
+int main(int argc, char *argv[])
+{
+
+ int my_rank;
+ int comm_sz;
+
+ int message_tag = 0;
+
+ MPI_Init(&argc, &argv);
+
+ const double A = atof(argv[1]);
+ const double B = atof(argv[2]);
+ const int N = atoi(argv[3]);
+
+ MPI_Comm_rank(MPI_COMM_WORLD, &my_rank);
+ MPI_Comm_size(MPI_COMM_WORLD, &comm_sz);
+
+ struct range r = new_range_2(my_rank, comm_sz, N);
+
+ double h = (B - A) / N;
+
+ int local_n = r.last - r.first;
+ double local_a = A + r.first * h;
+ double local_b = A + r.last * h;
+ double result_integral = trap(local_a, local_b, local_n);
+
+ printf("local n: %i local a: %f local b %f \n", local_n, local_a, local_b);
+
+ if (my_rank != 0)
+ {
+ MPI_Send(&result_integral, 1, MPI_DOUBLE, 0, message_tag, MPI_COMM_WORLD);
+ }
+ else // my_rank ==0
+ {
+
+ for (int source = 1; source < comm_sz; source++)
+ {
+ double result_integral_source;
+ MPI_Recv(&result_integral_source, 1, MPI_DOUBLE, source, message_tag, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
+ result_integral += result_integral_source;
+ }
+ printf("with n = %d trapezoids, our estimate\n", N);
+ printf("of the integral from %f to %f = %.15e\n", A, B, result_integral);
+ }
+
+ MPI_Finalize();
+
+ return 0;
+}
+
+double trap(double a, double b, long int n)
+{
+
+ double h = (b - a) / n;
+ // printf("H: %f\n",h);
+
+ double approx = (function(a) + function(b)) / 2.0;
+ for (int i = 1; i < n - 1; i++)
+ {
+ // printf("f_%i \n", i);
+ double x_i = a + i * h;
+
+ approx += function(x_i);
+ }
+
+ return h * approx;
+}
+
+double function(double x) { return x * x; }
+