This repository was archived by the owner on Mar 23, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontent.json
More file actions
1 lines (1 loc) · 111 KB
/
content.json
File metadata and controls
1 lines (1 loc) · 111 KB
1
{"posts":[{"title":"Lambda表达式","text":"Lambda 表达式能够捕获作用域中的变量的无名函数对象。 基本语法1[capture] (parameters) mutable -> return-type {statement} capture 捕获capture 部分指定哪些变量被捕获,以及是值捕获还是引用捕获。空的 capture 子句 [] 只是 lambda 表达式的主体不访问封闭范围中的变量。 可使用默认捕获模式:**&** 表示所有变量都通过引用的方式捕获;**=** 表示所有变量都通过值的方式捕获。 12345int a = 0; auto f = []() { return a * 9; }; // Error, 无法访问 'a'auto f = [a]() { return a * 9; }; // OK, 'a' 被值“捕获”auto f = [&a]() { return a++; }; // OK, 'a' 被引用“捕获” // 注意:请保证 Lambda 被调用时 a 没有被销毁auto b = f(); // f 从捕获列表里获得 a 的值,无需通过参数传入 a 调用 lambda 表达式时,不需要在传递参数了,因为可以由 lambda 表达式捕获,如上 auto b = f() 代码。 parameters 参数列表大多数情况下类似函数参数列表 12auto lam = [](int a, int b) { return a + b; };std::cout << lam(1, 9) << " " << lam(2, 6) << std::endl; parameters 参数列表 是可选的,如果不将参数传递给 Lambda 表达式,并且其 Lambda 声明器不包含 mutable,且没有后置返回值类型,则可以 省略空括号 。 参数列表可以用另外一个 lambda 表达式嵌套使用。 mutable 可变规范利用可变规范,Lambda 表达式的主体可以修改通过值捕获的变量。若使用此关键字,则 parameters 不可省略(即使为空)。 一个例子,使用 capture 捕获字句 中的例子,来观察 a 的值的变化: 12int a = 0;auto func = [a]() mutable { ++a; }; return-type 返回类型指定 lambda 表达式的返回类型,如果没有,将会自动推断。 statement lambda 主题和普通的函数体差不多。 1234567#include <iostream>int main() { int m = 0, n = 0; [&, n](int a) mutable { m = (++n) + a; }(4); std::cout << m << " " << n << std::endl; return 0;} 最后我们得到输出 5 0。这是由于 n 是通过值捕获的,在调用 Lambda 表达式后仍保持原来的值 0 不变。mutable 规范允许 n 在 Lambda 主体中被修改,将 mutable 删去则编译不通过。","link":"/2022/09/21/CPP/Lambda%E8%A1%A8%E8%BE%BE%E5%BC%8F/"},{"title":"C++ 11","text":"auto 使用 auto 必须初始化; auto 根据初始化的值来推导数据类型; 当类型不为引用时,auto 的推导结果将不保留表达式的 const 属性; 当类型为引用时,auto 的推导结果将保留表达式的 const 属性。 限制 不能再函数参数中使用 不能作用于类的非静态成员变量(没有 static 关键字的变量) 不能定义数组; 不能用于模板参数 应用 迭代器 泛型编程 decltype“declare type”的缩写。 和 auto 相似,用于自动的类型推导。 decltype(exp) varname = value;根据 exp 推出类型,而不是 value 。 不要求变量初始化。 推导 exp 是变量,则类型为变量 exp 是函数,则类型为函数返回类型 exp 是左值,则类型为 exp 的引用 应用 非静态成员 auto 和 decltype 区别 auto 可能不保留 const 等限定符,decltype 则会保留。 123456789101112//非指针非引用类型const int n1 = 0;auto n2 = 10;n2 = 99; //赋值不报错decltype(n1) n3 = 20;n3 = 5; //赋值报错//指针类型const int *p1 = &n1;auto p2 = p1;*p2 = 66; //赋值报错decltype(p1) p3 = p1;*p3 = 19; //赋值报错 对引用的处理 auto 抛弃引用类型,decltype 保留引用类型 12345678910111213141516171819#include <iostream>using namespace std;int main() { int n = 10; int& r1 = n; std::cout << r1 << ", " << &r1 << std::endl; // 10, 0x61fe04 std::cout << &n << std::endl; // 0x61fe04 // auto推导 auto r2 = r1; r2 = 20; cout << n << ", " << r1 << ", " << r2 << endl; // decltype推导 decltype(r1) r3 = n; std::cout << &r3 << std::endl; //0x61fe04 r3 = 99; cout << n << ", " << r1 << ", " << r3 << endl; return 0;} 模板深入理解模板 返回值类型后置 模板实例化中>> 符号的改进在 C++ 98/03 中,>>符号通常被编译器解释为右移操作符,通常会提示在两个尖括号中间添加空格。 现在 C++ 11 对模板的 >> 符号进行了单独处理,不再出问题。 当然在需要右移操作符时,最好用括号将表达式括起来。 使用 using 代替 typedef 定义别名12345678template <typename Val>struct str_map{ typedef std::map<std::string, Val> type;};// ...str_map<int>::type map1;// ... 改成: 1234template <typename Val>using str_map_t = std::map<std::string, Val>;// ...str_map_t<int> map1; typedef 具有语法一致性,但遇到复杂的会降低可读性 using 直接在后面用赋值的方式,更清晰 12345678910111213/* C++98/03 */template <typename T>struct func_t{ typedef void (*type)(T, T);};// 使用 func_t 模板func_t<int>::type xx_1;/* C++11 */template <typename T>using func_t = void (*)(T, T);// 使用 func_t 模板func_t<int> xx_2; 使用 using 可以轻松地创建一个新的模板别名,而不需要像 C++98/03 那样使用烦琐的外敷模板。 支持函数模板的默认模板参数 在函数模板和类模板中使用可变参数可变参数模板 tuple 元组详解 列表初始化 lambda 匿名函数Lambda 表达式 非受限联合体(union)1. C++11 允许非 POD 类型POD 类型一般具有以下几种特征(包括 class、union 和 struct 等): 没有用户自定义的构造函数、析构函数、拷贝构造函数和移动构造函数。 不能包含虚函数和虚基类。 非静态成员必须声明为 public。 类中的第一个非静态成员的类型与其基类不同,例如: class B1{};class B2 : B1 {B1 b;}; class B2 的第一个非静态成员 b 是基类类型,所以它不是 POD 类型。 在类或者结构体继承时,满足以下两种情况之一: 派生类中有非静态成员,且只有一个仅包含静态成员的基类; 基类有非静态成员,而派生类没有非静态成员。 所有非静态数据成员均和其基类也符合上述规则(递归定义),也就是说 POD 类型不能包含非 POD 类型的数据。 此外,所有兼容 C 语言的数据类型都是 POD 类型(struct、union 等不能违背上述规则)。 2. C++11 允许联合体有静态成员3. 注意C++11 规定,如果非受限联合体内有一个非 POD 的成员,而该成员拥有自定义的构造函数,那么这个非受限联合体的默认构造函数将被编译器删除;其他的特殊成员函数,例如默认拷贝构造函数、拷贝赋值操作符以及析构函数等,也将被删除。 这条规则可能导致对象构造失败。 例子: 12345678910#include <string>using namespace std;union U { string s; // string有自己的构造函数 int n;};int main() { U u; // 构造失败,因为 U 的构造函数被删除 return 0;} 解决问题要用到 placement new ,如下: 12345678910111213#include <string>using namespace std;union U { string s; int n;public: U() { new(&s) string; } ~U() { s.~string(); }};int main() { U u; return 0;} placement new语法:new(address) ClassConstruct(…) address 是内存地址。 ClassConstruct 表调用类的构造函数,如没有可省略括号 placement new 利用已经申请好的内存来生成对象,它不再为对象分配新的内存,而是将对象数据放在 address 指定的内存中。在本例中,placement new 使用的是 s 的内存空间。 非受限联合体的匿名声明和 “枚举式类” for 循环(基于范围的循环)详解C++ 11 : 123for (declaration : expression){// 循环体} 例子: 123456789101112131415161718#include <iostream>#include <vector>using namespace std;int main() { char arc[] = "http://c.biancheng.net/cplus/11/"; //for循环遍历普通数组 for (char ch : arc) { cout << ch; } cout << '!' << endl; vector<char>myvector(arc, arc + 23); //for循环遍历 vector 容器 for (auto ch : myvector) { cout << ch; } cout << '!'; return 0;} 注意事项 不管是遍历什么,即使是遍历容器,也不是遍历指向各个元素的迭代器,而是容器中的各个元素。 用范围的方式还可以遍历字符串 不支持遍历指针形式返回的数组(需要范围明确) 遍历 string 或容器时,遍历过程中函数只执行一次,如下 12345678910111213141516171819#include <iostream>#include <string>using namespace std;string str= "http://c.biancheng.net/cplus/11/";string retStr() { cout << "retStr:" << endl; return str;}int main() { //遍历函数返回的 string 字符串 for (char ch : retStr()) { cout << ch; } // output: // retStr: //http://c.biancheng.net/cplus/11/ return 0;} 使用基于范围的循环时,不修改容器中不允许被修改的部分 基于范围的循环,其底层也是借助容器迭代器实现 12345678910111213141516171819#include <iostream>#include <vector>int main(void){ std::vector<int>arr = { 1, 2, 3, 4, 5 }; for (auto val : arr) { std::cout << val << std::endl; arr.push_back(10); //向容器中添加元素 // 添加元素致使迭代器失效 } // 输出: //1 //-572662307 //-572662307 //4 //5 return 0;} long long 超长整型 右值引用 移动构造函数的功能和用法 move() 函数 引用限定符 完美转发及其实现 nullptr 初始化空指针 智能指针","link":"/2022/10/18/CPP/C++%2011/"},{"title":"右值引用","text":"左值左值可以被看作一个具有名称的内存位置。 特征: 能被取地址运算符获取地址 可修改的左值可用作内建赋值和内建复合赋值运算符的左操作数 可用来初始化左值引用 纯右值相当于 C++ 11 之前的右值。 将亡值引用引用 本质是别名,通过引用修改变量的值,传参时避免拷贝。 右值引用右值引用的标志为 && ,只能指向右值,不能指向左值。 123456int &&ref_a_right = 5; // okint a = 5;int &&ref_a_left = a; // 编译不过,右值引用不可以指向左值ref_a_right = 6; // 右值引用的用途:可以修改右值 左值引用与右值引用1. 右值引用指向左值使用 std::move 12345int a = 5; // a是个左值int &ref_a_left = a; // 左值引用指向左值int &&ref_a_right = std::move(a); // 通过std::move将左值转化为右值,可以被右值引用指向cout << a; // 打印结果:5 std::move 的功能就是强制把左值转换为右值,实现等同于一个类型转换:static_cast<T&& >(lvalue) 。 2. 左值引用、右值引用是什么值声明的 左值引用 和 右值引用 都是 左值 。 万能引用123456789101112// 使用 T&& param 的方式,传入左值引用或右值引用,可推导绑定左值或右值template<typename T>void func(T&& param) {cout << param << endl;}int main() {int num = 2019;func(num);func(2019);return 0;} 引用折叠一个模板函数,根据定义的形参和传入的实参的类型,我们可以有下面四中组合: 左值-左值 T& & # 函数定义的形参类型是左值引用,传入的实参是左值引用 左值-右值 T& && # 函数定义的形参类型是左值引用,传入的实参是右值引用 右值-左值 T&& & # 函数定义的形参类型是右值引用,传入的实参是左值引用 右值-右值 T&& && # 函数定义的形参类型是右值引用,传入的实参是右值引用 但是 C++中不允许对引用再进行引用,对于上述情况的处理有如下的规则: 所有的折叠引用最终都代表一个引用,要么是左值引用,要么是右值引用。规则是:如果任一引用为左值引用,则结果为左值引用。否则(即两个都是右值引用),结果为右值引用。 所以最后结果是: T& & = & T& && = & T&& & = & T&& && = && 当且仅当函数形参为右值引用且传入参数也为右值引用时,最终折叠后为右值引用 完美转发使用 std::forward 进行完美转发,std::forward 能够将正确的引用类型转发。 移动语义在进行对象的复制时,可能有两种需求:一种是深拷贝,开辟新的内存空间,拷贝原对象到这个新的内存空间;另一种则是通过右值引用,将原对象的成员通过修改标记的方式重新由新对象的成员指向,然后将原对象的所有成员清空。 调用赋值运算符时,也同理。","link":"/2022/09/20/CPP/%E5%8F%B3%E5%80%BC%E5%BC%95%E7%94%A8/"},{"title":"static关键字","text":"全局变量static 声明全局变量,不改变全局变量的存储位置与生命周期,仅改变全局变量的作用域,不被其他源文件通过 extern 调用。 局部变量static 会将局部变量的存储位置更改为静态存储区,生命周期伴随程序运行的整个过程。 普通函数普通函数不是类成员函数,使用 static 声明将限制函数的作用域,不被其他源文件通过 extern 调用;static 声明的函数在内存中仅存储一份拷贝。 一些优点: 避免了链接不同源文件时,发生变量名和函数名的冲突。 对 c 函数而言,对函数进行了信息屏蔽,只给当前文件的一些函数使用。 类 static 成员static 使成员由”属于类对象“变为”属于类,不属于类对象“ 数据成员 static 的数据成员需要在类定义体外部进行定义 12345678class BOOK{ private: //折扣 static float discount;};//在类定义体外部定义并初始化float BOOK::discount = 0.95f; 例外,当使用const static时,可以在类定义体内初始化数据成员,但仍需要在类定义体外进行一次定义。 1234567class BOOK{ private: //折扣 const static int discount=1;};const int BOOK::discount; 类成员函数在类内部声明的 static 成员函数,在类定义体外部不需要重复指定 static 关键字。 static 成员函数不能被声明为 const。","link":"/2022/09/22/CPP/static%E5%85%B3%E9%94%AE%E5%AD%97/"},{"title":"模板类之智能指针","text":"智能指针1. 定义行为类似于指针的类对象,但还有其他功能。 智能指针能够帮助管理动态内存分配 要创建智能指针对象,必须包含头文件 memory 。 有三类:auto_ptr , unique_ptr , shared_ptr 。其中 auto_ptr 已经被 C++11 摒弃。 2. 注意事项为什么会有三种智能指针? 三种智能指针可以处理一个问题,就是两个指针对象赋值后的内存释放问题:两个指针同时指向一个空间,那释放内存时同一块空间将被释放两次。 三种处理方式: 定义运算符,进行深拷贝 建立所有权(ownership),只有一个指针对象能够拥有这块内存空间,赋值操作后所有权转让。这是 auto_ptr , unique_ptr 的策略 创建智能更高的指针,跟踪引用这个对象的智能指针数量,称为引用计数(reference counting) 。例如赋值时,count+1,指针过期 delete 时,count-1。这是 shared_ptr 的策略 3. unique_ptr 为何优于 auto_ptrunique_ptr 会在编译阶段就报错, auto_ptr 则会在运行阶段使程序崩溃 这样一看,在编译阶段就报错的操作更好。 unique_ptr 是怎么解决呢,当把一个临时右值赋给智能指针对象时,赋值操作将合法。因为临时右值将会在复制后快速被销毁,就不会造成 指针悬挂 。 unique_ptr 还有一个有点。他有一个可用于数组的变体: delete 和 delete[] 配对 new 和 new[] 配对 而 auto_ptr 则没有。 4. 如何选择shared_ptr如果程序要使用多个指向同一个对象的指 针,应选择 shared_ptr。例子: 有一个指针数组,并使用一 些辅助指针来标识特定的元素,如最大的元素和最小的元素; 两个对象 包含都指向第三个对象的指针; STL 容器包含指针。 很多 STL 算法都支 持复制和赋值操作,这些操作可用于 shared_ptr,但不能用于 unique_ptr(编译器发出警告)和 auto_ptr(行为不确定)。如果您的编 译器没有提供 shared_ptr,可使用 Boost 库提供的 shared_ptr。 unique_ptr 如果程序不需要多个指向同一个对象的指针,则可使用 unique_ptr。 如果函数使用 new 分配内存,并返回指向该内存的指针,将其返回类型声明为 unique_ptr 是不错的选择。这样,所有权将转让给接受返回值的 unique_ptr,而该智能指针将负责调用 delete。 可将 unique_ptr 存储到 STL 容器中,只要不调用将一个 unique_ptr 复制或赋给另一个的 法或算法(如 sort( ))。 在满足 unique_ptr 要求的条件时,也可使用 auto_ptr,但 unique_ptr 是 更好的选择。如果您的编译器没有提供 unique_ptr,可考虑使用 BOOST 库提供的 scoped_ptr,它与 unique_ptr 类似。 weak_ptr1234567weak_ptr<T> w; //创建空 weak_ptr,可以指向类型为 T 的对象weak_ptr<T> w(sp); //与 shared_ptr 指向相同的对象,shared_ptr 引用计数不变。T必须能转换为 sp 指向的类型w=p; //p 可以是 shared_ptr 或 weak_ptr,赋值后 w 与 p 共享对象w.reset(); //将 w 置空w.use_count(); //返回与 w 共享对象的 shared_ptr 的数量w.expired(); //若 w.use_count() 为 0,返回 true,否则返回 falsew.lock(); //如果 expired() 为 true,返回一个空 shared_ptr,否则返回非空 shared_ptr 示例12345678910111213141516171819202122#include < assert.h>#include <iostream>#include <memory>#include <string>using namespace std;int main() { shared_ptr<int> sp(new int(10)); assert(sp.use_count() == 1); weak_ptr<int> wp(sp); // 从 shared_ptr 创建 weak_ptr assert(wp.use_count() == 1); if (!wp.expired()) { // 判断 weak_ptr 观察的对象是否失效 shared_ptr<int> sp2 = wp.lock(); // 获得一个 shared_ptr *sp2 = 100; assert(wp.use_count() == 2); } assert(wp.use_count() == 1); cout << "int:" << *sp << endl; return 0;}","link":"/2022/09/18/CPP/%E6%A8%A1%E6%9D%BF%E7%B1%BB%E4%B9%8B%E6%99%BA%E8%83%BD%E6%8C%87%E9%92%88/"},{"title":"深入理解模板","text":"模板是泛型编程的实现方式之一,只需要写一份便可以套用不同类型的类、函数、变量。 (以下涉及的代码有部分是从https://blog.csdn.net/m0_64361907/article/details/126788130 处转载) 模板函数这是普通的 c 语言写交换函数123456789101112131415161718192021222324#include<iostream>using namespace std;void Swapi(int* a, int* b){ int tmp = *a; *a = *b; *b = tmp;}void Swapd(double* a, double* b){ double tmp = *a; *a = *b; *b = tmp;}//……int main(){ int a = 1, b = 2; Swapi(&a, &b); double c = 1.1, d = 2.2; Swapd(&c, &d); return 0;} 可以看到,对于不同的参数类型,我们要写多个不同的交换函数。 而在 c 语言中,是不支持函数重载的,所以不同的交换函数无法使用同样的函数名实现重载 c++写交换函数123456789101112131415161718192021222324#include<iostream>using namespace std;void Swap(int& x, int& y){ int tmp = x; x = y; y = tmp;}void Swap(double& x, double& y){ double tmp = x; x = y; y = tmp;}//……int main(){ int a = 1, b = 2; Swap(a, b); double c = 1.1, d = 2.2; Swap(c, d); return 0;} C++ 中有了引用和函数重载,但是在实现不同类型参数的交换函数时仍然很麻烦。 所以就有了模板 使用模板的交换函数12345678910111213141516171819#include<iostream>using namespace std;template <class T>void Swap(T& x, T& y){ T tmp = x; x = y; y = tmp;}int main(){ int a = 1, b = 2; Swap(a, b); double c = 1.1, d = 2.2; Swap(c, d); return 0;} 如此,编译器能够自己套用对应的参数类型。 显示实例化和隐式实例化1234567891011121314151617181920212223#include<iostream>using namespace std;template <class T>T Add(const T& x,const T& y){ return x + y;}int main(){ int a = 1, b = 2; double c = 1.1, d = 2.2; cout << Add(a, b) << endl;//编译器要自己推类型的是隐式实例化 cout << Add(c, d) << endl; //cout << Add(a, c) << endl;//error这样的写法就错了,为难编译器了,编译器也推不出来了 cout << Add<int>(a, c) << endl;//不需要编译器去推的是显示实例化 cout << Add<double>(b, d) << endl; cout << Add(a, (int)c) << endl; return 0;} 不告诉编译器参数类型的是隐式实例化 指定了参数类型的是显式实例化 类模板此处将会以一个建议的 vector 容器的模板类实现作为例子。 实现 vector 落后的方式123456789101112131415typedef int VDateType;class vector{public: //……private: VDateType* _a; size_t _size; size_t _capacity;};int main(){ vector v1; vector v2; return 0;} 以前的实现方式是这样的,但我们不能让v1类型为 int,v2类型为 double。 使用类模板的方式1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798#include<iostream>#include<assert.h>using namespace std;namespace kcc{ template<class T> class vector{ public: vector() :_a(nullptr) , _size(0) , _capacity(0) {} // 拷贝构造和operator= 这里涉及深浅拷贝问题,还挺复杂,后面具体再讲 ~vector(){ delete[] _a; _a = nullptr; _size = _capacity = 0; } void push_back(const T& x){ if (_size == _capacity){ int newcapacity = _capacity == 0 ? 4 : _capacity * 2; T* tmp = new T[newcapacity]; if (_a){ memcpy(tmp, _a, sizeof(T) * _size); delete[] _a; } _a = tmp; _capacity = newcapacity; } _a[_size] = x; ++_size; } // 读+写 T& operator[](size_t pos); size_t size(); private: T* _a; size_t _size; size_t _capacity; }; // 模板不支持分离编译,也就是声明在.h ,定义在.cpp,原因后面再讲 // 建议就是定义在一个文件 xxx.h xxx.hpp // 在类外面定义 template<class T> T& vector<T>::operator[](size_t pos){ assert(pos < _size); return _a[pos]; } template<class T> size_t vector<T>::size(){ return _size; }}int main(){ kcc::vector<int> v1; // int v1.push_back(1); v1.push_back(2); v1.push_back(3); v1.push_back(4); // v1.operator[](3); //cout << v1[3] << endl; //cout << v1[5] << endl; for (size_t i = 0; i < v1.size(); ++i){ v1[i] *= 2; } cout << endl; for (size_t i = 0; i < v1.size(); ++i){ cout << v1[i] << " "; } cout << endl; kcc::vector<double> v2; // double v2.push_back(1.1); v2.push_back(2.2); v2.push_back(3.3); v2.push_back(4.4); for (size_t i = 0; i < v2.size(); ++i){ cout << v2[i] << " "; } cout << endl; return 0;}","link":"/2022/09/19/CPP/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%E6%A8%A1%E6%9D%BF/"},{"title":"虚函数和纯虚函数","text":"虚函数和纯虚函数定义虚函数类中,声明函数前有virtual关键字的为虚函数 123class A { virtual void example();} 纯虚函数纯虚函数的声明格式如下: 123class A { virtual void example() = 0;} 多态同样的入口,调用不同的函数。 从基类的对象作为入口,通过变换指针地址,调用派生类的函数。 基类的成员函数应该是虚函数,派生类中的成员函数可以是虚函数也可以是普通函数。 1234567891011121314151617181920212223242526272829303132333435// 多态的通常实现。#include <iostream>using namespace std;class A { public: virtual void Print() { cout << "A::Print" << endl; }};class B : public A { public: virtual void Print() { cout << "B::Print" << endl; }};class D : public A { public: virtual void Print() { cout << "D::Print" << endl; }};class E : public B { virtual void Print() { cout << "E::Print" << endl; }};int main() { A a; B b; D d; E e; A* pa = &a; B* pb = &b; pa->Print(); // 多态, a.Print()被调用,输出:A::Print pa = pb; // 基类指针pa指向派生类对象b pa->Print(); // b.Print()被调用,输出:B::Print pa = &d; // 基类指针pa指向派生类对象d pa->Print(); // 多态, d. Print ()被调用,输出:D::Print pa = &e; // 基类指针pa指向派生类对象e pa->Print(); // 多态, e.Print () 被调用,输出:E::Print return 0;} 通过基类引用实现多态1234567891011121314151617181920#include <iostream>using namespace std;class A { public: virtual void Print() { cout << "A::Print" << endl; }};class B : public A { public: virtual void Print() { cout << "B::Print" << endl; }};void Printlnfo(A& r) { r.Print(); // 多态,调用哪个Print,取决于r引用了哪个类的对象}int main() { A a; B b; Printlnfo(a); // 输出 A::Print Printlnfo(b); // 输出 B::Print return 0;} 函数重写和函数重载函数重写(override)派生类中重新定义函数,只有函数体中实现的内容可以和被重写的函数不同。 基类被重写的函数应该是虚函数或纯虚函数。 函数重载函数同名,但参数的个数,类型_,_顺序 可以不同,调用时根据参数列表选择函数。 重载不关心函数返回类型。 例子12345678910111213141516171819202122232425262728293031323334class A {public: virtual void intro(){ cout<<"I am a funtion of class A."; }};class B : public A {public: void intro(){ cout<<"I am a funtion of class B."; } void intro(int a){ cout<<"I am a funtion of class B with input param int."; }}int main(void){ std::shared_ptr<A> a = std::make_shared<A>(); //! 调用A::intro() a->intro(); a.reset(new B()); //! 调用B::intro(), B::intro()重写了A::intro() a->intro(); B b; //! 调用B::intro(int a), B::intro(int a)重载了B::intro() b.intro(1); return 0;}","link":"/2022/09/19/CPP/%E8%99%9A%E5%87%BD%E6%95%B0%E5%92%8C%E7%BA%AF%E8%99%9A%E5%87%BD%E6%95%B0/"},{"title":"cuda1","text":"CUDA 介绍CPU :面向延迟设计image-20210607010547778(C:\\Users\\Aerialith\\AppData\\Roaming\\Typora\\typora-user-images\\image-20210607010547778.png ALU 减少操作延迟 Cache 将长延迟内存访问转换为短延迟缓存访问 控制模块 - 分支预测以减少分支延迟 - 数据转发以减少数据延迟 GPU:面向吞吐量设计image-20210607010747373(C:\\Users\\Aerialith\\AppData\\Roaming\\Typora\\typora-user-images\\image-20210607010747373.png 小缓存 提高内存吞吐量 简单控制 无分支预测 无数据转发 高效的 ALU 许多、长延迟但大量流水线以实现高吞吐量 需要大量线程来容忍延迟 线程逻辑 线程状态 并行代码,GPU 比 CPU 快 10 倍以上,串行代码,CPU 比 GPU 快 10 倍以上 CUDA 编程模型编程模型是底层计算机系统的抽象,它允许表达算法和数据结构。语言和 API 提供了这些抽象的实现,并允许将算法和数据结构付诸实践——编程模型的存在独立于编程语言和支持 API 的选择。 一些设计目标 扩展到 100 个内核、1000 个并行线程 让程序员专注于并行算法不是并行编程语言的机制。 启用异构系统(即 CPU+GPU)CPU 和 GPU 是具有独立 DRAM 的独立设备 关键并行抽象 并发线程的层次结构 轻量级同步原语 协作线程的共享内存模型 线程层次结构 线程 thread - 由 CUDA 运行时分发(由 threadIdx 标识) Warp – 最多 32 个线程的调度单元 块 block – 用户定义的 1 到 512 个线程组。(由 blockIdx 标识) 网格 grid – 一组一个或多个块。 为每个 CUDA 核函数创建一个网格 cuda 内存层次结构 寄存器 每个线程内存用于自动变量和寄存器溢出。 共享内存 每块低延迟内存,允许块内数据共享和同步。 线程可以通过这块内存安全地共享数据,并且可以通过 _ _syncthreads() 进行屏障同步 全局内存 可以在块或网格之间共享的设备级内存 硬件Tesla 架构的主要组件是:流式多处理器(8800 有 16 个)标量处理器内存层次结构互联网络主机接口 流式多处理器 Streaming Multiprocessor (SM)image-20210607012204300(C:\\Users\\Aerialith\\AppData\\Roaming\\Typora\\typora-user-images\\image-20210607012204300.png 每个 SM 有 8 个标量处理器 (SP)Each SM has 8 Scalar Processors (SP) IEEE 754 32 位浮点支持(不完全支持)- IEEE 754 32-bit floating point support (incomplete support) 每个 SP 是一个 1.35 GHz 处理器(32 GFLOPS 峰值)- Each SP is a 1.35 GHz processor (32 GFLOPS peak) 支持 32 位和 64 位整数- Supports 32 and 64 bit integers 8,192 个动态分区的 32 位寄存器- 8,192 dynamically partitioned 32-bit registers 硬件支持 768 个线程(32 个线程的 24 个 SIMT 经线)- Supports 768 threads in hardware (24 SIMT warps of 32 threads) 在硬件中完成的线程调度- Thread scheduling done in hardware 16KB 低延迟共享内存- 16KB of low-latency shared memory 2 个特殊函数单元(平方根倒数、三角函数等)- 2 Special Function Units (reciprocal square root, trig functions, etc) 数据并行 - 向量加法示例image-20210607155913228(C:\\Users\\Aerialith\\AppData\\Roaming\\Typora\\typora-user-images\\image-20210607155913228.png 1234567891011121314// Compute vector sum C = A + Bvoid vecAdd(float *h_A, float *h_B, float *h_C, int n){ int i; for (i = 0; i<n; i++) h_C[i] = h_A[i] + h_B[i];}int main(){ // Memory allocation for h_A, h_B, and h_C // I/O to read h_A and h_B, N elements … vecAdd(h_A, h_B, h_C, N);} cudaMalloc() 在设备全局内存中分配一个对象 两个参数 指向已分配对象的指针的地址 已分配对象的大小(以字节为单位) cudaFree() 从设备全局内存中释放对象 一个参数 指向释放对象的指针 cudaMemcpy() 内存数据传输 需要四个参数 指向目的地的指针 指向源的指针 复制的字节数 转移类型/方向 向量加法主机代码123456789101112131415void vecAdd(float *h_A, float *h_B, float *h_C, int n){ int size = n * sizeof(float); float *d_A, *d_B, *d_C; cudaMalloc((void **) &d_A, size); cudaMemcpy(d_A, h_A, size, cudaMemcpyHostToDevice); cudaMalloc((void **) &d_B, size); cudaMemcpy(d_B, h_B, size, cudaMemcpyHostToDevice); cudaMalloc((void **) &d_C, size); // Kernel invocation code – to be shown later cudaMemcpy(h_C, d_C, size, cudaMemcpyDeviceToHost); cudaFree(d_A); cudaFree(d_B); cudaFree (d_C);} cuda 执行模式异构主机(CPU)+设备(GPU)应用 C 程序 主机 C 代码中的串行部分 设备 SPMD 内核代码中的并行部分 image-20210607160519981(C:\\Users\\Aerialith\\AppData\\Roaming\\Typora\\typora-user-images\\image-20210607160519981.png ISA 级别的程序 程序是存储在内存中的一组指令,可由硬件读取、解释和执行。 CPU 和 GPU 都是基于(不同的)指令集设计的 程序指令对存储在存储器和/或寄存器中的数据进行操作。 作为冯诺依曼处理器的线程线程是“虚拟化的”或“抽象的” image-20210607160745684(C:\\Users\\Aerialith\\AppData\\Roaming\\Typora\\typora-user-images\\image-20210607160745684.png 并行线程数组 CUDA 内核由线程网格(数组)执行 网格中的所有线程都运行相同的内核代码(SPMD,Single Program Multiple Data) 每个线程都有用于计算内存地址和做出控制决策的索引 image-20210607160922707(C:\\Users\\Aerialith\\AppData\\Roaming\\Typora\\typora-user-images\\image-20210607160922707.png 线程块:可扩展的合作image-20210607160955464(C:\\Users\\Aerialith\\AppData\\Roaming\\Typora\\typora-user-images\\image-20210607160955464.png 将线程数组分成多个块 块内的线程通过共享内存、原子操作和屏障同步进行协作 不同块中的线程不交互 blockIdx and threadIdx 每个线程使用索引来决定要处理的数据 blockIdx:1D、2D 或 3D (CUDA 4.0) threadIdx:1D、2D 或 3D 处理多维数据时简化内存寻址 图像处理 求解体积上的偏微分方程 … image-20210607161116355(C:\\Users\\Aerialith\\AppData\\Roaming\\Typora\\typora-user-images\\image-20210607161116355.png NVCC 编译器 NVIDIA 提供了一个 CUDA-C 编译器 nvcc NVCC 编译设备代码,然后将代码转发到主机编译器(例如 g++) 可用于编译和链接 host only 应用程序","link":"/2019/12/07/CUDA/cuda1/"},{"title":"cuda2","text":"多维内核多维内核配置示例image-20210607161352581(C:\\Users\\Aerialith\\AppData\\Roaming\\Typora\\typora-user-images\\image-20210607161352581.png 处理 2D 网格的图片C/C++ 中的行优先布局 PictureKernel 的源代码12345678910__global__ void PictureKernel(float* d_Pin, float* d_Pout, int height, int width){ // Calculate the row # of the d_Pin and d_Pout element int Row = blockIdx.y*blockDim.y + threadIdx.y; // Calculate the column # of the d_Pin and d_Pout element int Col = blockIdx.x*blockDim.x + threadIdx.x; // each thread computes one element of d_Pout if in range if ((Row < height) && (Col < width)) { d_Pout[Row*width+Col] = 2.0*d_Pin[Row*width+Col]; }} 用于启动 PictureKernel 的主机代码12345678// assume that the picture is m × n,// m pixels in y dimension and n pixels in x dimension// input d_Pin has been allocated on and copied to device// output d_Pout has been allocated on device…dim3 DimGrid((n-1)/16 + 1, (m-1)/16+1, 1);dim3 DimBlock(16, 16, 1);PictureKernel<<<DimGrid,DimBlock>>>(d_Pin, d_Pout, m, n); 用 16x16 的块覆盖 62x76 的图片![image-20210607161715736(C:\\Users\\Aerialith\\AppData\\Roaming\\Typora\\typora-user-images\\image-20210607161715736.png 并非 Block 中的所有线程都将遵循相同的控制流路径。 彩色到灰度图像处理示例RGB 图像 图像中的每个像素都是一个 RGB 值 图像行的格式是 (r g b) (r g b) … (r g b) RGB 范围分布不均 RGB 转灰度图像灰度数字图像是其中每个像素的值仅携带强度信息的图像。 颜色计算公式 对于 (I, J) 处的每个像素 (r g b),执行: grayPixel[I,J] = 0.21r + 0.71g + 0.07*b 这只是一个点积 <[r,g,b],[0.21,0.71,0.07]> 常量特定于输入 RGB 空间 RGB 转灰度代码123456789101112131415161718192021#define CHANNELS 3 // we have 3 channels corresponding to RGB// The input image is encoded as unsigned characters [0, 255]__global__ void colorConvert(unsigned char * grayImage, unsigned char * rgbImage, int width, int height) { int x = threadIdx.x + blockIdx.x * blockDim.x; int y = threadIdx.y + blockIdx.y * blockDim.y; if (x < width && y < height) { // get 1D coordinate for the grayscale image int grayOffset = y*width + x; // one can think of the RGB image having // CHANNEL times columns than the gray scale image int rgbOffset = grayOffset*CHANNELS; unsigned char r = rgbImage[rgbOffset ]; // red value for pixel unsigned char g = rgbImage[rgbOffset + 2]; // green value for pixel unsigned char b = rgbImage[rgbOffset + 3]; // blue value for pixel // perform the rescaling and store it // We multiply by floating point constants grayImage[grayOffset] = 0.21f*r + 0.71f*g + 0.07f*b; }} 12345678910111213141516171819202122#define CHANNELS 3 // we have 3 channels corresponding to RGB// The input image is encoded as unsigned characters [0, 255]__global__ void colorConvert(unsigned char * grayImage, unsigned char * rgbImage, int width, int height) { int x = threadIdx.x + blockIdx.x * blockDim.x; int y = threadIdx.y + blockIdx.y * blockDim.y; if (x < width && y < height) { // get 1D coordinate for the grayscale image int grayOffset = y*width + x; // one can think of the RGB image having // CHANNEL times columns than the gray scale image int rgbOffset = grayOffset*CHANNELS; unsigned char r = rgbImage[rgbOffset ]; // red value for pixel unsigned char g = rgbImage[rgbOffset + 1]; // green value for pixel unsigned char b = rgbImage[rgbOffset + 2]; // blue value for pixel // perform the rescaling and store it // We multiply by floating point constants grayImage[grayOffset] = 0.21f*r + 0.71f*g + 0.07f*b; }} 图像模糊模糊框img src=”C:\\Users\\Aerialith\\AppData\\Roaming\\Typora\\typora-user-images\\image-20210607162547106.png” alt=”image-20210607162547106” style=”zoom: 80%;” /> 2D 内核的图像模糊12345678__global__ void blurKernel(unsigned char * in, unsigned char * out, int w, int h) { int Col = blockIdx.x * blockDim.x + threadIdx.x; int Row = blockIdx.y * blockDim.y + threadIdx.y; if (Col < w && Row < h) { ... // Rest of our kernel } } 12345678910111213141516171819202122232425__global__ void blurKernel(unsigned char * in, unsigned char * out, int w, int h) { int Col = blockIdx.x * blockDim.x + threadIdx.x; int Row = blockIdx.y * blockDim.y + threadIdx.y; if (Col < w && Row < h) { int pixVal = 0; int pixels = 0; // Get the average of the surrounding 2xBLUR_SIZE x 2xBLUR_SIZE box for(int blurRow = -BLUR_SIZE; blurRow < BLUR_SIZE+1; ++blurRow) { for(int blurCol = -BLUR_SIZE; blurCol < BLUR_SIZE+1; ++blurCol) { int curRow = Row + blurRow; int curCol = Col + blurCol; // Verify we have a valid image pixel if(curRow > -1 && curRow < h && curCol > -1 && curCol < w) { pixVal += in[curRow * w + curCol]; pixels++; // Keep track of number of pixels in the accumulated total } } } // Write our new pixel value out out[Row * w + Col] = (unsigned char)(pixVal / pixels); } } 线程调度 每个块可以相对于其他块以任何顺序执行。 硬件可以随时自由地将块分配给任何处理器 内核可扩展到任意数量的并行处理器 示例:执行线程块 线程以块粒度分配给流式多处理器 (SM) 在资源允许的情况下,每个 SM 最多 8 个块 Fermi SM 最多可以占用 1536 个线程 可以是 256(线程/块)* 6 块 或 512(线程/块)* 3 个块等。 SM 维护线程/块 idx # s SM 管理/调度线程执行 ![image-20210607164925138(C:\\Users\\Aerialith\\AppData\\Roaming\\Typora\\typora-user-images\\image-20210607164925138.png 具有 SIMD 单元的 Von-Neumann 模型![image-20210607164946076(C:\\Users\\Aerialith\\AppData\\Roaming\\Typora\\typora-user-images\\image-20210607164946076.png 作为调度单位的 Warp 每个 Block 作为 32 线程 Warps 执行 实施决策,不属于 CUDA 编程模型的一部分 Warps 是 SM 中的基本调度单元 未来的 GPU 可能在每个 warp 中有不同数量的线程 ![image-20210607165046866(C:\\Users\\Aerialith\\AppData\\Roaming\\Typora\\typora-user-images\\image-20210607165046866.png 线程调度 warp 中的线程在 SIMD 中执行 选中时,warp 中的所有线程都执行相同的指令 N 路路径 →1/N 吞吐量(应在同一路径内拓展分支) SM 实现零开销 warp 调度 其下一条指令的操作数已准备好供使用的 Warps 有资格执行 根据优先级调度策略选择合格的 Warps 进行执行 ![image-20210607165216727(C:\\Users\\Aerialith\\AppData\\Roaming\\Typora\\typora-user-images\\image-20210607165216727.png warp 示例 如果给一个 SM 分配了 3 个块,每个块有 256 个线程,那么一个 SM 中有多少个 Warp? 每个 Block 分为 256/32 = 8 Warps 有 8 * 3 = 24 个 warp 块粒度注意事项 对于使用多个块的矩阵乘法,我应该为 Fermi 使用 8X8、16X16 还是 32X32 块? 对于 8X8,我们每个块有 64 个线程。 由于每个 SM 最多可以占用 1536 个线程,这相当于 24 个块。 但是,每个 SM 最多只能占用 8 个 Blocks,每个 SM 只能有 512 个线程! 对于 16X16,我们每个块有 256 个线程。 由于每个 SM 最多可以占用 1536 个线程,因此它最多可以占用 6 个块并实现满容量,除非其他资源考虑无效。 对于 32X32,我们每个块有 1024 个线程。 费米的 SM 中只能容纳一个块。 仅使用 SM 线程容量的 2/3。","link":"/2021/06/07/CUDA/cuda2/"},{"title":"cuda3","text":"内存和数据局部性示例 – 矩阵乘法 #### 一个基本的矩阵乘法 123456789101112131415__global__ void MatrixMulKernel(float* M, float* N, float* P, int Width) { // Calculate the row index of the P element and M int Row = blockIdx.y*blockDim.y+threadIdx.y; // Calculate the column index of P and N int Col = blockIdx.x*blockDim.x+threadIdx.x; if ((Row < Width) && (Col < Width)) { float Pvalue = 0; // each thread computes one element of the block sub-matrix for (int k = 0; k < Width; ++k) { Pvalue += M[Row*Width+k]*N[k*Width+Col]; } P[Row*Width+Col] = Pvalue; }} GPU 上的性能如何 CGMA ratio(Compute to Global Memory Access ratio):每次访问 CUDA 内核区域内全局内存执行的浮点计算次数。 越大越好 基本矩阵乘法的 CGMACGMA 就是看你取一次数,多少次运算需要用到这个数的值 所有线程为其输入矩阵元素访问全局内存 每次浮点加法一次内存访问(4 字节) CGMA ratio =1 4B/s 的内存带宽/FLOPS 假设一个 GPU 峰值浮点速率 1,500 GFLOPS,200 GB/s DRAM 带宽 4*1,500 = 6,000 GB/s 需要达到峰值 FLOPS 200 GB/s 的内存带宽将执行速度限制在 50 GFLOPS 这将执行率限制为设备峰值浮点执行率的 3.3% (50/1500)! 需要大幅减少内存访问以接近 1,500 GFLOPS要达到 1,500 GFLOPS 的峰值,我们需要 CGMA=30 如何提高内存访问效率? 增加计算量 提高利用率 利用内存层次结构 全局内存 共享内存 寄存器文件 声明 CUDA 变量 Variable declaration Memory Scope Lifetime int LocalVar; register thread thread device shared int SharedVar; shared block block device int GlobalVar; global grid application device constant int ConstantVar; constant grid application ** device ** 在与 ** shared ** 或 ** constant ** 一起使用时是可选的 自动变量驻留在寄存器中 除了驻留在全局内存中的每线程数组 示例:共享内存变量声明1234void blurKernel(unsigned char * in, unsigned char * out, int w, int h) { __shared__ float ds_in[TILE_WIDTH][TILE_WIDTH]; …} CUDA 中的共享内存 一种特殊类型的内存,其内容在内核源代码中明确定义和使用 每个 SM 一个 以比全局内存高得多的速度(在延迟和吞吐量方面)访问 访问和共享范围——线程块 Lifetime——线程阻塞,对应线程结束执行后内容会消失 通过内存加载/存储指令访问 计算机体系结构中的一种暂存存储器形式 基本矩阵乘法内核的全局内存访问模式 分块/阻塞 - 基本理念 将全局内存内容划分为 tiles 将线程的计算集中在每个时间点的一个或少量分块上 分块的基本概念 在拥堵的交通系统中,显着减少车辆可以大大改善所有车辆看到的延迟 为通勤者拼车 全局内存访问的平铺 司机 = 访问其内存数据操作数的线程 汽车 = 内存访问请求 一些计算对分块更具挑战性 有些拼车可能比其他拼车更容易 拼车参与者需要有类似的工作时间表 有些车辆可能更适合拼车 分块也存在类似的挑战 需要同步: 大纲 识别由多个线程访问的全局内存内容块 将 tile 从全局内存加载到片上内存中 使用屏障同步来确保所有线程都准备好开始阶段 让多个线程从片上存储器访问它们的数据 使用屏障同步来确保所有线程都完成了当前阶段 移动到下一个 tile •Tiled Matrix Multiplication Kernel矩阵乘法数据访问模式 每个线程 - 一行 M 和一列 N 每个线程块 – 一条 M 条和一条 N 条 分块矩阵乘法 将每个线程的执行分解成阶段 这样线程块在每个阶段的数据访问都集中在 M 的一个 tile 和 N 的一个 tile 上 tile 在每个维度中包含 BLOCK_SIZE 个元素 加载 tile一个块中的所有线程都参与每个线程在平铺代码中加载一个 M 元素和一个 N 元素 屏障同步 同步块中的所有线程 __syncthreads() 同一个块中的所有线程必须到达 __syncthreads() 才能继续前进 最适合用于协调分阶段执行平铺算法 确保在阶段开始时加载 tile 的所有元素 确保在阶段结束时消耗 tile 的所有元素 处理分块的边界条件 平铺矩阵乘法内核123456789101112131415161718192021222324__global__ void MatrixMulKernel(float* M, float* N, float* P, Int Width){ __shared__ float ds_M[TILE_WIDTH][TILE_WIDTH]; __shared__ float ds_N[TILE_WIDTH][TILE_WIDTH]; int bx = blockIdx.x; int by = blockIdx.y; int tx = threadIdx.x; int ty = threadIdx.y; int Row = by * blockDim.y + ty; int Col = bx * blockDim.x + tx; float Pvalue = 0; // Loop over the M and N tiles required to compute the P element for (int p = 0; p < WIDHT/TILE_WIDTH; ++p) { // Collaborative loading of M and N tiles into shared memory ds_M[ty][tx] = M[Row*Width + p*TILE_WIDTH+tx]; ds_N[ty][tx] = N[(p*TILE_WIDTH+ty)*Width + Col]; __syncthreads(); for (int i = 0; i < TILE_WIDTH; ++i)Pvalue += ds_M[ty][i] * ds_N[i][tx]; __synchthreads(); } P[Row*Width+Col] = Pvalue;} 分块(线程块)大小注意事项 每个线程块应该有多个线程16 的 TILE_WIDTH 给出 1616 = 256 个线程32 的 TILE_WIDTH 给出 3232 = 1024 个线程 For 16, in each phase, each block performs 2*256 = 512 float loads from global memory for 256 * (2*16) = 8,192 mul/add operations. (16 floating-point operations for each memory load)CGMA=? For 32, in each phase, each block performs 2*1024 = 2048 float loads from global memory for 1024 * (2*32) = 65,536 mul/add operations. (32 floating-point operation for each memory load)CGMA=? 共享内存和线程 For an SM with 16KB shared memory Shared memory size is implementation dependent! For TILE_WIDTH = 16, each thread block uses 22564B = 2KB of shared memory. For 16KB shared memory, one can potentially have up to 8 thread blocks executing This allows up to 8512 = 4,096 pending loads. (2 per thread, 256 threads per block) The next TILE_WIDTH 32 would lead to 232324 Byte= 8K Byte shared memory usage per thread block, allowing 2 thread blocks active at the same time However, the thread count limitation of 1536 threads per SM in current generation GPUs will reduce the number of blocks per SM to one! Each __syncthread() can reduce the number of active threads for a block More thread blocks can be advantageous 对于具有 16KB 共享内存的 SM 共享内存大小取决于实现! 对于 TILE_WIDTH = 16,每个线程块使用 22564B = 2KB 的共享内存。 对于 16KB 共享内存,最多可能有 8 个线程块在执行 这允许多达 8*512 = 4,096 个待处理负载。 (每个线程 2 个,每个块 256 个线程) 下一个 TILE_WIDTH 32 将导致每个线程块使用 232324 Byte= 8K Byte 共享内存,从而允许 2 个线程块同时处于活动状态 但是,当前一代 GPU 中每个 SM 1536 个线程的线程数限制将使每个 SM 的块数减少到一个! 每个 __syncthread() 可以减少一个块的活动线程数 更多的线程块可能是有利的 处理任意大小的矩阵–Threads that do not calculate valid P elements but still need to participate in loading the input tiles –Phase 0 of Block(1,1), Thread(1,0), assigned to calculate non-existent P[3,2] but need to participate in loading tile element N[1,2] – –Threads that calculate valid P elements may attempt to load non-existing input elements when loading input tiles –Phase 0 of Block(0,0), Thread(1,0), assigned to calculate valid P[1,0] but attempts to load non-existing N[3,0] 不计算有效 P 元素但仍需要参与加载输入瓦片的线程Block(1,1)的 Phase 0, Thread(1,0),分配计算不存在的 P[3,2]但需要参与加载 tile 元素 N[1,2] 计算有效 P 元素的线程可能会在加载输入图块时尝试加载不存在的输入元素Block(0,0)、Thread(1,0) 的第 0 阶段,分配用于计算有效 P[1,0] 但尝试加载不存在的 N[3,0] “简单”的解决方案–When a thread is to load any input element, test if it is in the valid index range –If valid, proceed to load –Else, do not load, just write a 0 –Rationale: a 0 value will ensure that that the multiply-add step does not affect the final value of the output element –The condition tested for loading input elements is different from the test for calculating output P element –A thread that does not calculate valid P element can still participate in loading input tile elements Loading Elements – with boundary check123456789101112138 for (int p = 0; p < (Width-1) / TILE_WIDTH + 1; ++p) {++ if(Row < Width && t * TILE_WIDTH+tx < Width) {9 ds_M[ty][tx] = M[Row * Width + p * TILE_WIDTH + tx];++ } else {++ ds_M[ty][tx] = 0.0;++ }++ if (p*TILE_WIDTH+ty < Width && Col < Width) {10 ds_N[ty][tx] = N[(p*TILE_WIDTH + ty) * Width + Col];++ } else {++ ds_N[ty][tx] = 0.0;++ }11 __syncthreads(); Inner Product – Before and After12345678910++ if(Row < Width && Col < Width) {12 for (int i = 0; i < TILE_WIDTH; ++i) {13 Pvalue += ds_M[ty][i] * ds_N[i][tx]; }14 __syncthreads();15 } /* end of outer for loop */++ if (Row < Width && Col < Width)16 P[Row*Width + Col] = Pvalue; } /* end of kernel */ 一些要点 对于每个线程,条件是不同的 加载 M 元素 加载 N 元素 计算和存储输出元素 对于大矩阵,控制发散的影响应该很小 处理一般矩形矩阵一般来说,矩阵乘法是根据矩形矩阵定义的j x k M 矩阵乘以 k x l N 矩阵产生 j x l P 矩阵 我们已经介绍了方阵乘法,一种特殊情况 核函数需要泛化处理一般矩形矩阵Width 参数被三个参数替换:j、k、l当 Width 用于指代 M 的高度或 P 的高度时,用 j 代替当 Width 用于指代 M 的宽度或 N 的高度时,替换为 k当 Width 用于指代 N 的宽度或 P 的宽度时,替换为 l","link":"/2021/06/08/CUDA/cuda3/"},{"title":"cuda4","text":"性能Warps and SIMD一个线程 block 由 32 个线程 warp 组成扭曲在多处理器上以物理方式并行执行 (SIMD) Warp 是调度单位 控制分支 当 warp 中的线程通过做出不同的控制决策而采取不同的控制流路径时,就会发生控制分支 一些采用 then 路径,另一些采用 if 语句的 else 路径 一些线程与其他线程采用不同数量的循环迭代 采取不同路径的线程的执行在当前的 GPU 中被序列化 一个 warp 中的线程所采用的控制路径一次遍历一个,直到不再存在。 考虑嵌套控制流语句时,不同路径的数量可能很大 控制分支例子当分支或循环条件是线程索引的函数时,可能会出现分歧 具有分歧的内核语句示例:如果 (threadIdx.x > 2) { }这为块中的线程创建了两个不同的控制路径决策粒度 < 扭曲大小; 线程 0、1 和 2 遵循与第一个 warp 中的其余线程不同的路径 没有发散的例子:如果 (blockIdx.x > 2) { }决策粒度是块大小的倍数; 任何给定 warp 中的所有线程都遵循相同的路径 控制分支的性能影响 边界条件检查对于并行代码的完整功能和健壮性至关重要 分块矩阵乘法内核有很多边界条件检查 令人担忧的是,这些检查可能会导致性能显着下降 12345678910if(Row < Width && p * TILE_WIDTH+tx < Width) { ds_M[ty][tx] = M[Row * Width + p * TILE_WIDTH + tx];} else { ds_M[ty][tx] = 0.0;}if (p*TILE_WIDTH+ty < Width && Col < Width) { ds_N[ty][tx] = N[(p*TILE_WIDTH + ty) * Width + Col];} else { ds_N[ty][tx] = 0.0; 加载 M Tiles 时的两种块 直到最后一个阶段,其 tiles 都在有效范围内的块。 方块有部分一直在有效范围之外 控制分支影响分析假设 16x16 tiles 和线程块每个线程块有 8 个 warp (256/32)假设 100x100 的方阵每个线程将经历 7 个阶段(上限为 100/16) 有 49 个线程块(每个维度 7 个) 加载 M tiles 的控制分支TYPE1假设 16x16 TILES 和线程块每个线程块有 8 个 WARP (256/32)假设 100x100 的方阵每个经线将经历 7 个阶段(100/16 的上限) 有 42($67$)个类型 1 块,总共有 336($842$)条 warps它们都有 7 个阶段,因此有 2,352 (336*7) 个 WARP 阶段经线只有在最后阶段才有控制发散336 个经线阶段有控制分支 7 个阶段:每行取七次,最后一次不完整 只考虑 Warp 不考虑 Block 不完整:因为 Block 不完整会导致整个 Warps 都不取,也就不存在分支 336 个阶段:6*7*8*1 6*7 个 block,每个 8 个 Warp TYPE2类型 2:分配加载底部 TILES 的 7 个块,共 56($87$)个扭曲它们都有 7 个阶段,所以有 392 ($567$) 个 WARP 阶段每个类型 2 块中的前 2 个 WARP 将保持在有效范围内,直到最后一个阶段剩余的 6 个 WARP 不在有效范围内所以,只有 14 (2*7) 个经线阶段有控制分支 14 个阶段:2*7*1 7 个 block,每个 2 个 Warp 2 个 Warp:两个横排,一个横排 16 个 在大矩阵情况下,对于性能影响很小 控制分支总体影响类型 1: 块:2,352 个 warp 阶段中的 336 个具有控制分支类型 2: 块:392 个 warp 阶段中有 14 个具有控制分支性能影响预计小于 12% (350/2,944 或 (336+14)/(2352+14)) Add。加载 N 个 TILEs 时控制发散的影响计算有些不同,留作练习 估计的性能影响取决于数据。对于较大的矩阵,影响将显着较小 一般来说,控制发散对大型输入数据集的边界条件检查的影响应该是微不足道的应该毫不犹豫地使用边界检查来确保完整的功能 内核中充满控制流结构的事实并不意味着会出现严重的控制发散 我们将在 Parallel Algorithm Patterns 模块中介绍一些自然会导致控制发散(例如并行缩减)的算法模式 并行规约划分和总结将数据集分成更小的块让每个线程处理一个块使用归约树将每个块的结果汇总为最终答案 将大的问题分解成小的问题,让每个线程负责一个问题,并利用一棵树将结果归约为最终结果。 Reduction Conputation规约将一组输入的数组汇总成一个值,例如: 求最值 求和 积 算法复杂度 o(N) 并行求和规约每个线程负责两个值的求和,需要 n/2 个线程,执行 log(n)次。 in-place 不使用辅助变量来转换输入数据结构 一个简单的数据映射线程每个线程负责部分和向量的偶数索引位置(位置责任)每一步后,不再需要一半的线程输入之一总是来自责任地点在每一步中,其中一个输入来自越来越远的距离 12345678910111213__shared__ float partialSum[2*BLOCK_SIZE];unsigned int t = threadIdx.x;unsigned int start = 2*blockIdx.x*blockDim.x;partialSum[t] = input[start + t];partialSum[blockDim.x+t] = input[start + blockDim.x+t];for (unsigned int stride = 1; stride <= blockDim.x; stride *= 2){ __syncthreads(); if (t % stride == 0) partialSum[2*t]+= partialSum[2*t+stride];} 同步是因为需要在进行下一步前,获得上一步的所有结果,下一步的操作数来源是新的 求和完成后,如果 Block 非常多,宿主代码可以迭代启动另一个内核进行求和;若较少,则可以传回主机 加和,或利用原子操作累加到全局变量中。 优化每次迭代后 Warp 中真正参与运算的线程很少,资源利用率非常低,在 5 次之后每个 Warp 中只有一个线程在运行但却占用了整个 Warp 的资源 通过改变索引改善,使得部分和压缩在数组的前面位置 在一些算法中,可以改变索引的使用来改善发散行为交换和结合运算符始终将部分和压缩到 partialSum[] 数组中的前面位置保持活动线程连续 更好的核函数1234567for (unsigned int stride = blockDim.x; stride > 0; stride /= 2){ __syncthreads(); if (t < stride) partialSum[t] += partialSum[t+stride];} 内存并行全局内存(DRAM)带宽 DRAM 核心阵列组织 DRAM 核心阵列很慢–DDR: Core speed = ½ interface speed –DDR2/GDDR3: Core speed = ¼ interface speed –DDR3/GDDR4: Core speed = ⅛ interface speed DRAM Bursting (突发)通过将 N 倍位宽的数据加载至缓冲区,随后以 N 步读出(仅适用于连续地址) 复数 Bank 时类似 将内存地址划分为几个不同的区域,当一个地址被读取,整个区域被送出。 内存合并因此,当一个 Warp 中的所有线程执行一个 load 时,且访问位在同一个突发区域中时,只会发出一个读取 指令,且访问合并。快。若不是这样,就会发出多个请求,并且一些读出的数据被丢弃。 如果数组访问中的索引采用以下形式,则扭曲中的访问是对连续位置的访问 _A[(expression with terms independent of threadIdx.x) + threadIdx.x];_( 中间英文:具有独立项的表达式)","link":"/2021/06/09/CUDA/cuda4/"},{"title":"cuda5","text":"直方图parallel histogramA simple parallel histogram algorithm 将输入分成几部分让每个线程获取输入的一部分每个线程遍历其部分。对于每个字母,增加适当的 bin 计数器 输入分区影响内存访问效率 分段分区导致内存访问效率低下 相邻线程不访问相邻内存位置 访问未合并 DRAM 带宽利用率低 解决更改为交错分区所有线程处理元素的连续部分他们都移动到下一部分并重复内存访问被合并 数据竞争发生在读-修改-写过程中,导致无法预计的错误。 多个线程同时操作一些一样的变量,造成了竞争,出错 使用原子操作可以避免。 原子操作由单个硬件指令对存储器位置执行读-修改-写操作,硬件确保当前原子操作完成之前没有其他线程可以 完成读-修改-写操作,会维护一个队列。 12 int atomicAdd(int* address, int val);//原子加 addr+val,写回addr more12345678910Unsigned 32-bit integer atomic addunsigned int atomicAdd(unsigned int* address, unsigned int val);Unsigned 64-bit integer atomic addunsigned long long int atomicAdd(unsigned long long int* address, unsigned long long int val);Single-precision floating-point atomic add (capability > 2.0)float atomicAdd(float* address, float val); 基本直方图内核1234567891011121314__global__ void histo_kernel(unsigned char *buffer,long size, unsigned int *histo) { int i = threadIdx.x + blockIdx.x * blockDim.x;// stride is total number of threads int stride = blockDim.x * gridDim.x; // All threads handle blockDim.x * gridDim.x // consecutive elements whileint alphabet_position = buffer[i] – “a”; if (alphabet_position >= 0 && alpha_position < 26) atomicAdd(&(histo[alphabet_position/4]), 1); i += stride; }} 原子操作 DRAM对 DRAM 位置的原子操作从读取开始,其延迟为数百个周期原子操作以写入同一位置结束,延迟数百个周期在这整个过程中,没有其他人可以访问该位置 延迟决定吞吐量同一 DRAM 位置上原子操作的吞吐量是应用程序可以执行原子操作的速率。特定位置上的原子操作速率受读取-修改-写入序列的总延迟限制,对于全局存储器 (DRAM) 位置通常超过 1000 个周期。这意味着,如果许多线程尝试在同一位置(争用)上执行原子操作,则内存吞吐量将减少到 < 一个内存通道峰值带宽的 1/1000! Fermi L2 缓存 原子操作中等延迟,约为 DRAM 延迟的 1/10在所有块之间共享全局内存原子的“免费改进” 共享内存原子操作非常短的延迟每个线程块私有需要程序员的算法工作(稍后详述)手动编程 私有化副本有缺点创建和初始化需要开销,将私有化写入最终副本需要开销 但访问和串行化代价更小,总体性能提高 10 倍以上 如果直方图太大无法私有化可以部分私有化 私有化的成本和收益成本创建和初始化私有副本的开销将私人副本的内容累积到最终副本的开销 受益访问私有副本和最终副本时的争用和序列化要少得多整体性能通常可以提高 10 倍以上 直方图的共享内存原子操作(共享内存需要私有化每个线程子集都在同一个块中吞吐量远高于 DRAM (100x) 或 L2 (10x) 原子减少争用——只有同一块中的线程才能访问共享内存变量这是共享内存的一个非常重要的用例! 一些其他的东西私有化是用于并行化应用程序的强大且常用的技术 私有直方图大小需要很小适合共享内存 如果直方图太大而无法私有化怎么办?有时可以部分私有化输出直方图并使用范围测试转到全局内存或共享内存 一些数据集在局部区域有大量相同的数据值一个简单而有效的优化是每个线程在更新直方图的相同元素时将连续更新聚合为单个更新","link":"/2021/06/10/CUDA/cuda5/"},{"title":"cuda6","text":"SCAN前缀和 串行12for(j=1;j<n;j++) out[j] = out[j-1] + f(j); 并行12forall(j) { temp[j] = f(j) }; scan(out, temp); 工作效率低下的扫描内核优化方案 复杂度为 nlog(n) 迭代 log(n)次,每次运算数量级为 n 每次迭代都需要同步已确保输入为最新值 1234567891011121314__global__ void work_inefficient_scan_kernel(float *X, float *Y, int InputSize) { __shared__ float XY[SECTION_SIZE]; int i = blockIdx.x * blockDim.x + threadIdx.x; if (i < InputSize) {XY[threadIdx.x] = X[i];} // the code below performs iterative scan on XY for (unsigned int stride = 1; stride <= threadIdx.x; stride *= 2) { __syncthreads(); float in1 = XY[threadIdx.x - stride]; __syncthreads(); XY[threadIdx.x] += in1; } __ syncthreads(); If (i < InputSize) {Y[i] = XY[threadIdx.x];}} 但仍劣于串行算法 此扫描执行 log(n) 次并行迭代迭代 do (n-1), (n-2), (n-4),..(n- n/2) 添加每个总和:n * log(n) - (n-1) O(n*log(n)) 工作 更优的算法将输入转化为平衡树,从下至上扫描树至根,根即为所有叶子综合,部分和记录在树中。 1234567891011121314151617// XY[2*BLOCK_SIZE] is in shared memory//上部分for (unsigned int stride = 1;stride <= BLOCK_SIZE; stride *= 2) { int index = (threadIdx.x+1)*stride*2 - 1; if(index < 2*BLOCK_SIZE) XY[index] += XY[index-stride]; __syncthreads();} for (unsigned int stride = BLOCK_SIZE/2; stride > 0; stride /= 2) { __syncthreads(); int index = (threadIdx.x+1)*stride*2 - 1; if(index+stride < 2*BLOCK_SIZE) { XY[index + stride] += XY[index]; } } __syncthreads(); if (i < InputSize) Y[i] = XY[threadIdx.x]; 复杂度规约时执行 log(n)次迭代,每次数量从 n/2 以 1/2 递减至 1,随后反规约迭代 log(n)-1 次。 等比数列,2(n-1) 工作效率高的内核在缩减步骤中执行 log(n) 次并行迭代迭代做 n/2, n/4,..1 添加总加:(n-1) O(n) 工作 它在减少后的反向步骤中执行 log(n)-1 并行迭代迭代做 2-1, 4-1, …. n/2-1 添加总加:(n-2) – (log(n)-1) O(n) 工作 一些讨论对比第一种优化方式绝对性能更好,因为只需要一个方向的规约。 第二种优化方式更理想,效率更高,占用资源更少 大量输入对于两种方案都是将输入分块,得到块总和后再以相同算法扫描 Exclusive Scan第一个元素赋 0,其他元素后移即可 OpenACCopen accelerators 为简化在异构 CPU/GPU 开发而出现 OpenACC 设备结构 运行层次向量:包括多个线程以锁步形式工作(SIMD/SIMT) Worker:共同计算一个向量 Gang:一个或多个 Workers 共享资源 占有率计算GPU 占用率衡量 GPU 计算资源的利用率。 How much parallelism is running / How much parallelism the hardware could run 基本指令(DIRECTIVE)OpenACC loop directive: gang, worker, vector, seqThe loop directive gives the compiler additional information about the next loop. gang – Apply gang-level parallelism to this loop worker – Apply worker-level parallelism to this loop vector – Apply vector-level parallelism to this loop seq – Do not apply parallelism to this loop, run it sequentially Multiple levels can be applied to the same loop, but the levels must be applied in a top-down order. 指令格式#pragma acc directive-name [clause-list] new-line#pragma acc 编译指示名 [子句[ [,] 子句]…] 换行 OpenACC kernels Directive OpenACC Data movement directive OpenACC Loop","link":"/2021/06/11/CUDA/cuda6/"},{"title":"Hexo安装","text":"Hexo是什么Hexo 是一个快速、简洁且高效的博客框架。Hexo 使用 Markdown(或其他渲染引擎)解析文章,在几秒内,即可利用靓丽的主题生成静态网页。 安装 安装 nodejs 和 git首先是安装安装 nodejs ,这是使用 Hexo 必备的组件,安装 nodejs 将会同时安装 npm 。 来到 nodejs 官网 进行下载。 nodejs 下载完成后,来到 git 官网 下载 git。 git 能够起到从 github 下载项目并解压,方便使用 它还可以为你把 Blog 项目挂载到 github 上,此后便可以通过互联网访问你的博客,本篇将暂时不会写这个。 安装 Hexo安装完以上两种组件后,我们可以直接来到 Hexo 官网 。 官网首先就是安装代码: 1npm install hexo-cli -g 在终端中输入这行代码,Hexo 将会自行完成安装。 基本操作初始化博客文件夹现在你的计算机中选择一个你想要用来存放 Hexo 博客源文件的地方。然后输入以下代码。 1234567891011121314hexo init blog//初始化一个名为 blog 的文件夹,这是你博客源文件存放的地方。//blog可以修改为任意你想要的名称。cd blog// 进入创建并初始化好的博客文件夹npm install// 安装必备的组件hexo server// 启动服务器// 你可以通过打开终端中的链接:http://localhost:4000/ 来打开初始化的博客网站 new12hexo new "post title with whitespace"// 简单示例 使用 new 命令新建一篇新的文章,scaffold 文件夹中有模板,默认使用 post.md 作为初始模板。 此时你就做得到写文章写文章了。对于 markdown 格式的文章,需要使用到 obsidian 、 vscode( vscode 需要进一步的设置和下载插件来达到预览等功能)等编辑器来编写。 12hexo n -p Hexo/Icarus安装 "Icarus安装"// 这条命令可以帮助在本地创建文件夹后,将新建的文章创建在指定的文件夹。 该命令具体的其他参数请参考官方文档。 generate123hexo generatehexo g // 简写 生成静态文件 serverdeploy123hexo deployhexo d //简写 cleanclean1hexo clean 清除缓存文件 (db.json) 和已生成的静态文件 (public)。 安装完成安装完成后,我们可以运行下面这段代码: 123hexo server// 或者hexo s 我们第一次运行我们安装好的博客,Ctrl + 鼠标左键 能够打开运行在本地的博客网页地址。 然后我们就得到了我们的博客,它以默认的主题和一篇基本文章显示。 参考链接Hexo 官网: https://hexo.io/zh-cn/","link":"/2023/02/06/Hexo/Hexo%E5%AE%89%E8%A3%85/"},{"title":"Icarus安装","text":"前言通过前面的文章,已经学习了 Hexo 博客的安装和基本操作。本篇将解决主题的安装问题。 Hexo 有不少的主题可以挑选,官网的 主题页面 有 300+ 的主题可供选择。 我选择的是比较干净简介且符合我审美的 Icarus 主题 。这是一个分为三栏的博客主题,黑白灰还有蓝色的搭配深得我意。 开始安装接下来我们开始安装,来到 Icarus 主题的 Github 页面,有安装方式,我这里也写一下。 123456npm install hexo-theme-icarus// 首先进行安装hexo config theme icarus// 然后把主题设置为Icarus 其中主题的修改也可以找到 _config.yml 文件中,找到 theme 选项,将其修改为 theme: icarus 。 这样我们就完成了 Icarus 主题的安装。","link":"/2023/02/09/Hexo/Icarus%E5%AE%89%E8%A3%85/"},{"title":"分布式chap-3","text":"第三章 并行程序设计Foster 方法划分、通信:处理与机器无关的问题,影响并发性和可扩展性聚合、映射:处理与机器有关的问题,影响局部性和其他性能问题 划分将要执行的指令和数据按计算部分拆分成多个小任务。关键在于识别出可以并行执行的任务 通信确定划分的任务之间需要执行哪些通信 聚合将前面确定的任务与通信结合成更大的任务。例如任务 A 必须在任务 B 之前执行,那么把他们聚合成一个简单符合任务将会比较明智 映射将聚合好的任务分配到进程/线程中。要使通信量最小化,是各个进程/线程分配的工作量大致均衡 划分区域划分 划分数据尽可能等大 生成多个任务,每个都有一些数据和数据操作集 一些经验:首先关注最大的数据结构或最常访问的数据结构 功能划分 区域划分的补充 任务的数据需求可能脱节或者明显重叠 功能划分通常通过流水线实现并发任务集合 区域划分最自然,如果首先用功能划分,则并行算法作为整体会更简单 通信局部通信 每个任务仅与少量的相邻任务通信 创建说明数据流的通道 全局通信 大量相邻任务和远程任务都提供数据来执行计算 通常在设计早期,不为他们创建通道 可能导致通信过多或限制并发执行的机会 纯本地通信的算法中,有两个问题可能会阻碍并行执行: 算法是集中的:他不分布计算和通信,单个任务必须参与每个操作 算法是顺序的:不允许多个计算和通信操作同时进行 常用方法: 分布式通信与计算 分而治之 通信方式 1分布式通信与计算 一种求和算法,N 个任务相连,以便对分布在这些任务中的 N 个数字求和 单个求和仍需 N-1 步,但只有当多个求和操作要执行是,才允许并发执行 分而治之 划分成两个或多个大小大致相同的较简单问题。 划分产生的子问题能够并行解决时,分治算法是有效的 logN 步 通信方式 2结构化通信 一个任务和他邻近的任务形成一个规则结构,比如树或网格 非结构化通信 网络可以是任意图 使聚合和映射复杂化 如果通信需求是动态的,则程序运行时需频繁应用负载平衡算法,并且必须权衡这些算法的成本与收益 通信总结 任务间通信是并行算法开销的一部分 最小化并行开销是并行算法设计的一个重要目标 聚合聚合时,考虑合并是否有用,并确定复制数据和/或计算是否值得 目标 提高性能 保持程序可扩展性 简化编程 提高性能可以降低通信成本,a:消除通信;b:减少通信次数 更少的任务创建成本和任务调度成本 可扩展性8x128x256,2、3 维聚合,有一 4CPU 的处理器,可以分为 2x128x256 来执行;8 个 CPU,分 1x128x256;超过 8 个 CPU,可移植性受损害 讨论 关于聚集和复制,这三个目标导向的决策有时会产生冲突。 通过增加计算和通信力度以降低通信成本,在可伸缩性和映射的决策方面保持灵活性,并降低软件的工程成本 增加粒度 通过发送更少的数据减少通信时间。即使发送相同数量的数据,也可通过使用更少的消息来实现 还可以降低任务创建成本 方法:表面对体积效应;复制计算;避免通信 表面对体机效应 一个任务的通信需求与它所操作的子域的表面成正比,而计算需求与子域的体积成正比。 复制计算 保持灵活性 为了具有可移植性和可扩展性,创建不同数量的任务的能力很重要。 在为特定计算机调整代码时,这种灵活性也很有用 如果任务在等待远程数据时经常阻塞,那么将多个任务映射到一个处理器是有益的 重叠计算和通信:一个任务的通信和另一个任务的计算重叠 创建比处理器更多的任务为在可用处理器上平衡计算负载的映射策略提供了更大的范围 降低软件工程成本 并行化现有的代码时,另一个问题是与不同划分策略相关的相对开发成本 从这个角度看,最有趣的策略可能是那些避免大量代码修改的策略 映射分配任务给处理器的过程。 开发映射算法的目标通常是最小化总执行时间。 冲突目标: 最大化处理器利用率,即系统处理器积极解决问题所需任务的平均时间百分比 最小化处理器间通信 使通信次数最少,并且每个进程/线程得到大致相同的工作量 最佳映射: 寻找最佳映射是一个 NP-Hard 必须依赖于启发式 映射决策树静态任务数 结构化通信 每个任务的恒定计算时间 最小化通信的方式聚合任务 每个处理器创建一个任务 每个任务的可变计算时间 循环映射任务到处理器 非结构化通信 使用静态负载平衡算法 动态任务树 任务间频繁通信 使用动态负载平衡算法 许多短期任务(没有任务间通信) 使用运行时任务调度算法 案例边界值问题 PPTchap.3 49 页最大值问题 74 页n 体问题 82 页添加数据输入 90 页","link":"/2021/06/01/%E5%88%86%E5%B8%83%E5%BC%8F/%E5%88%86%E5%B8%83%E5%BC%8Fchap-3/"},{"title":"分布式chap.1","text":"第一章 Architecture大纲(outline) 冯诺依曼模型的修改 分布式硬件 弗林分类法 共享内存系统和分布式存储系统 内存层次结构和缓存一致性 互联网络 分布式软件 输入输出 CPU 与存储器分离是冯诺依曼模型的瓶颈 改进冯诺依曼模型: 缓存 虚拟内存 低级并行 缓存基础 缓存:可以比其他一些内存位置用更少的时间访问的内存位置的集合。 CPU 高速缓存:CPU 可以比其访问主内存更快地访问的内存位置的集合。 CPU 高速缓存可以与 CPU 位于同一芯片上,也可以位于与普通存储芯片相比访问速度更快的单独芯片上。 缓存的想法 使用更广泛的互连,并在单个内存访问中传输更多数据或更多指令。 将数据块和指令块存储在实际上更靠近 CPU 寄存器的特殊内存中。 哪些数据或指令放 cache? 程序倾向使用物理上接近最近使用的数据和指令。 局部性原理(系统使用更宽的互联结构访问数据和指令) 局部性 - 访问一个位置,然后访问附近的位置 在访问一个内存位置后,程序将在不久的将来(时间局部性)访问附近的位置(空间局部性)。 为了利用局部性原理,系统使用更广泛的有效互连来访问数据和指令。(缓存块或缓存行) 时间局部性 空间局部性 cache 分级:L1:最快最小L2:L3:最慢最大 Cache 查找由 L1 往下查找,Cache 中有信息,则命中;信息没找到,则称缺失;那么程序将会从主存读出查找的信息。 缓存写入策略CPU 往 cache 写数据,cache 中的值和主存中的值不一致。解决:写直达(writing-through):写入 cache 时,立即写入主存。写回(writing-back):将数据更新的 cache line 标记为脏(dirty),然后 cache line 缓存替换时,dirty的行被写入主存。 Cache 映射 全相联(fully associative) 每个 cache line 可以放置在 cache 的任何位置 直接映射(directed mapped) 每个 cache line 在 cache 中有唯一位置 n 路组相联(n-way set associative)折中方案 每个 cache line 可以放置到 cache 中 n 个不同区域中的一个 .lfphpbqxrlbf{zoom:75%;} 内存中的行能映射到 cache 中的多个不同位置时,如何决定替换或驱逐哪一行?常用最近最少使用方案 虚拟存储器大型程序,数据和指令集主存可能放不下,这时就是用虚拟存储器(虚拟内存)。把当前程序用到的放入主存,暂时不需要的放入辅存。虚拟内存也是对数据块和指令块进行操作,通常称为页。辅存较主存慢非常多,所以也的大小通常比较大。 指令级并行流水线将功能分成多个单独的硬件或功能单元, 多发射一个时钟周期发射多条指令,一般处理器有多个累加器或乘法器以实现多发射 编译时调度,静态多发射 运行时调度,动态多发射,超标量 为了能够多发射,需要用到预测, 硬件多线程线程级并行提供粗粒度的并行性,TLP 的线程 对 ILP 的指令 硬件多线程任务阻塞,线程快速切换,继续其他任务 同步多线程细粒度多线程变种,允许多个线程同时使用多个功能单元来利用超标量处理器的性能。优先线程:有多条指令就绪的线程,能减轻线程减速的问题。 并行硬件SISDSIMD(好单指令多数据,一个控制单元和多个 ALU,每个 ALU 要么在当前数据上执行同一个指令,要么空闲,下一条指令前需等待广播;通过在处理期间划分数据以提高并行性 向量处理器 对数组或数据向量操作;向量寄存器:存储多个操作数组成的向量,并能对其内容进行操作向量化和流水化的功能单元:相同操作应用于向量中的每个元素向量指令:向量上的操作交叉存储器:内存系统有多个内存体组成,访问一个内存体后,想再访问他有延迟,访问其他的内存体则很快步长式存储器访问和硬件散射/聚集操作:固定步长访问向量元素;对无规律间隔的数据进行读(聚集)、写(散射) 优点: 速度快,易于使用 向量编译器擅长识别向量化代码 识别不能向量化的循环并提供不能向量化的原因 高内存带宽 每个加载的数据都使用 缺点: 可扩展性有限 不能处理不规则的数据结构 MISD MIMD(好多指令多数据,包括一组完全独立的处理单元或核,每个处理单元或核都有自己的控制单元和 ALU;通常是异步的,即各个处理器能按照他们的节奏运行;没有全局时钟,不同处理器的系统时间没有联系,除非强制同步。 共享内存系统一个或者多核处理器,通过互联网络,与内存系统相互连接,处理器可以访问每个内存区域,处理器之间隐式通信。通常每个核都拥有私有的 L1cache,其他级 cache 可能共享也可能不共享; 一致内存访问 互连网络将所有处理器直接连接到主存,每个处理器访问内存任意区域的时间相同 非一致内存访问 互连网络将每个处理器连接到一块内存,通过处理器中的硬件达到访问内存中其他块的目的,0 号处理器访问直接连接的 0 号内存速度快,0 号处理器访问 1 号处理器直接连接的 1 号内存速度慢。 分布式内存系统分布式内存系统又称集群:由一组商品化系统组成,通过商品化网络连接(以太网)。每个系统中的处理器连接系统内的自己的内存,每个系统通过互连网络进行显式通信。 互连网络对两种内存系统有影响 共享内存互连网络 共享内存系统,最常用总线和交叉开关矩阵 总线 交叉开关矩阵 使用交换器控制数据传递,线表示双向链路。允许在不同设备间通信,速度比总线快;开销相对较高,成本也比较高。 分布式内存互连网络 直接互连 交换器处理器一对一,交换机之间相互连接,形成一个环或者环面网格。比总线高级,因为允许多个通信同时发生。环面网格比环成本高,因为交换器更复杂。 等分宽度: 衡量同时通信的链路数目或连接性的标准,计算链路数量 链路带宽: 传输数据的速度 等分带宽: 衡量网络质量,计算链路带宽。 最理想直接互联网络是:全相连网络等分宽度:p^2/4节点数量多不可能做这样的连接,因为他总共需要 p^2/4+p/2 条链路,不切实际。所以一般作为衡量其他互连网络的基础 超立方体:用于实际系统中的高度互连的直接互连网络,递归构造。一维超立方体:全互连系统;二维超立方体:由两个一维组成,通过相应交换器相连,形成一个正方形;n 维超立方体有p=2^n个节点,每个节点和 n 个节点相连,等分宽度为p/2,连接性更高,但需要更强大交换器,以支持连线数量。编号方法:直接相连的节点,编号只改变一位,例如 000 连 001、010、100。 间接互连 通常由单向连接和一组处理器组成,每个处理器有一个输入链路和一个输出链路,链路通过交换网络连接 交叉开关矩阵: 类似共享内存系统中的交叉开关矩阵,但把双向链路改为单向链路,使用p^2个交换器。 omega 网络: 交换器是 2x2 的交叉开关矩阵。成本较交叉开关网络低。共用了**1/2plog2^(p)个交换器,因为 2x2,所以总共是2plog2^(p)**个。 胖树: 延迟与带宽消息传送时间 = l + n / b 延迟(latency):发送源开始传输数据到目的地接收数据的时间。 带宽(bandwidth):目的地开始接收数据的速度。 内存结构层次多个层次,cpu 内 cache 有多级,内存延迟, Cache 一致性每个 CPU 有自己的 Cache,所以在进行对数据的修改时,会遇到一致性的问题,过时的数据仍存储在 CPU 中。如果多 CPU 对一个 Cache 则不会,但因为是串行访问 cache,速度会慢。 一致性协议直写和写回 处理器 A 和处理器 B,各自本地的 Cache Line 有同一个变量的拷贝,此时该 Cache Line 处于Shared状态。处理器 A 在本地修改了变量,除了把本地变量的 Cache Line 改为Modified状态外,还需要在处理器 B 读同一个变量前把处理器 B 存这个变量的 Cache Line 改为Invalid状态。后续处理器 B 需要对这个变量读写时,会 Cache Miss。会重新从内存拷贝新的数据到 CacheLine 中。 监听 Cache 一致性协议想法基于总线系统,监听总线,看到变量更新,则广播,并标记过时的变量为非法。由于每次监听到改变都要广播,广播开销很大。 基于目录的 Cache 一致性协议通过目录的数据结构,目录存储每行的状态,对应每个 Cache Line,更新 Cache,对应行的目录项就会更新。当变量要更新时,就会查目录,并将包含这个变量的 cache 置为非法状态。 伪共享多个 CPU 的多个线程同时修改自己的变量。。。 并行软件软件是负担, SPMD(single program,multiple data):仅包含一段可执行代码,通过使用条件转移语句,让这段代码在执行时表现得像是在不同处理器上执行的程序。 进程或线程的协调 分配任务,每个进程/线程分配大致相同的工作量,且使得通信量最小 安排进程/线程之间的同步 安排进程/线程之间的通信 共享内存动态线程共享内存程序使用。主线程等待任务,派生出新的线程,线程处理结束,被终止合并回主线程 静态线程派生所有的线程,工作结束前,所有线程都在运行,所有线程合并回主线程后,主线程做清理工作(释放内存),然后终止。 静态线程比动态线程性能更好,但可能会浪费系统资源(对资源的利用不是很高效) 非确定性定义:给定的输入产生不同的输出。MIMD 中,异步运行可能引发。 竞争条件 临界区 互斥 互斥锁(增强了临界区串行性) 忙等待 分布式内存消息传递1234567891011char message [ 1 0 0 ] ;. . .my_rank = Get_rank ( ) ;if ( my_rank == 1) { sprintf ( message , "Greetings from process 1" ) ; Send ( message , MSG_CHAR , 100 , 0 ) ;} else if ( my_rank == 0) { Receive ( message , MSG_CHAR , 100 , 1 ) ; printf ( "Process 0 > Received: %s\\n" , message ) ;} 程序段是 SPMD,两个程序使用相同的可执行代码,但执行不同的操作。执行的操作以来与他们的序号。 不同进程中,变量指的是不同的内存块 单向通信 单向通信或远程内存访问中,单个处理器调用一个函数 能够简化通信,大大降低通信成本 实践难以实现 划分全局地址空间的语言(PGAS 语言)允许用户使用共享内存技术来对分布式内存硬件进行编程。 私有变量在运行程序的核的局部内存空间中分配,共享数据结构中数据的分配由程序员分配。 输入与输出并行程序输入输出时,制定一些规则 在分布式内存程序中,只有进程 0 能够访问 stdin。共享内存程序中,只有主线程可以访问。 分布式内存和共享内存系统中,所有进程/线程都可以访问 stdout 和 stderr 因为输出到 stdout 的非确定顺序,一般只有一个进程/线程回将结果输出到 stdout。输出调试程序的结构是例外,它可以多个进程/线程写到 stdout 只有一个进程/线程你会尝试访问一个除 stdin、stdout 或者 stderr 以外的文件。例如每个进程/线程能打开自己私有的文件进行读/写,但没有两个进程/线程能打开相同的文件。 调试程序输出在生成输出结果时,应该包括进程/线程的序号或进程标识符。","link":"/2021/05/30/%E5%88%86%E5%B8%83%E5%BC%8F/%E5%88%86%E5%B8%83%E5%BC%8Fchap-1/"},{"title":"素描课","text":"第一张 第二张 第三张 第四章 第五张 第六张 第七张 第八张 第九张 第十张 期末考","link":"/2021/05/26/%E5%90%83%E5%96%9D%E7%8E%A9%E4%B9%90/%E7%B4%A0%E6%8F%8F%E8%AF%BE/"},{"title":"分布式chap-4","text":"第四章 性能指标(加速比和效率) 运行时间(执行时间) 加速 效率 可扩展性 便携性、编程能力等等 超线性加速表达式 固有串行计算 σ(n) 潜在并行计算 (n) 并行开销 κ(n,p) 加速比Ts:串行时间Tp:并行时间 效率Ep = Sp/p 阿姆达尔定律 实例: 局限性:忽略了并行的开销,高估了可实现的加速回顾:将问题大小视为常数显示执行时间如何随着处理器数量的增加而减少 其他角度我们经常使用更快的计算机来解决更大的问题实例让我们将时间视为常数,并允许问题规模随着处理器数量的增加而增加 古斯塔夫定律阿姆达尔和古斯塔夫都忽略了并行开销,所以有另一个定律 Karp-Flatt将并行开销包含考虑,并检测加速模型中忽略的其他开销 进程启动时间 进程同步时间 不平衡的工作量 结构开销 等效率指标 并行系统:在并行计算机上执行的并行程序 并行系统的可扩展性:衡量其随着处理器数量增加而提高性能的能力 可扩展的系统在添加处理器时保持效率 等效率:衡量可扩展性的方法 等效率推导步骤 从加速公式开始 计算总开销 假设效率保持不变 确定顺序执行时间和开销之间的关系 可扩展性功能 假设等效率关系为 n >= f(p) 令 M(n) 表示大小为 n 的问题所需的内存 M(f(p))/p 显示每个处理器的内存使用量必须如何增加才能保持相同的效率 我们称 M(f(p))/p 为可伸缩性函数 可扩展性函数的含义 为了在增加 p 时保持效率,我们必须增加 n 受可用内存限制的最大问题大小,在 p 中是线性的 可扩展性函数显示每个处理器的内存使用量必须如何增长才能保持效率 可扩展性函数常数意味着并行系统是完全可扩展的 复杂度例子 串行算法复杂度 T(n,1) = (n) 并行算法 计算复杂度 = (n/p) 通信复杂度 = (log p) 并行开销 T0(n,p) = (p log p) 弗洛伊德算法 顺序时间复杂度:(n3) 并行计算时间:(n3/p) 并行通信时间:(n2log p) 并行开销:T0(n,p) = (pn2log p) 等效率关系 n3 C(p n2 log p) n C p log pM(n) = n2并行系统可扩展性差 有限差分 每次迭代的顺序时间复杂度:(n2) 每次迭代的并行通信复杂度:(n/p) 并行开销:(n p) 等效率关系 n2 Cnp n C pM(n) = n2 该算法具有完美的可扩展性","link":"/2021/06/01/%E5%88%86%E5%B8%83%E5%BC%8F/%E5%88%86%E5%B8%83%E5%BC%8Fchap-4/"},{"title":"分布式chap-5","text":"消息传递编程进程 启动时指定数量 在整个程序执行过程中保持不变 都执行相同的程序 每个都有唯一的 ID 号 交替执行计算和通信 消息传递模型的优势 使程序员能够管理内存层次结构 许多架构的可移植性 更容易创建确定性程序 简化调试 消息传递接口历史 1980 年代后期:供应商拥有独特的库 1989 年:在橡树岭国家实验室开发的并行虚拟机 (PVM) 1992 年:开始制定 MPI 标准 1994 年:MPI 标准 1.0 版 1997 年:MPI 标准 2.0 版 今天:MPI 是主要的消息传递库标准 聚合与映射 并行算法的特性 固定任务数 任务之间没有通信 每个任务所需的时间是可变的 查阅映射策略决策树 以循环方式将任务映射到处理器 循环(交错)分配假设 p 个进程每个进程得到每 pth 件工作示例:5 道工序和 12 件工作P0: 0, 5, 10P1:1、6、11P2: 2, 7P3: 3, 8P4:4、9 编程头文件123456789//mpi依赖#include<mpi.h>#include<stdio.h>int main (int argc, char *argv[]) { int i; int id; /* Process rank */ int p; /* Number of processes */ void check_circuit (int, int); argc 和 argv:初始化 MPI 需要它们 运行此程序的每个进程的每个变量的一份副本 初始化1MPI_Init (&argc, &argv); 每个进程调用的第一个 MPI 函数 不一定是第一个可执行语句 允许系统进行任何必要的设置 Communicator 通信器:为进程提供消息传递环境的不透明对象MPI_COMM_WORLD默认的通讯器包括所有进程可以创建新的通信器将在第 8 章和第 9 章中执行此操作 确定进程数1MPI_Comm_size (MPI_COMM_WORLD, &p); 第一个参数是 Communicator 通信器第二个是返回的进程数 进程数序号1MPI_Comm_rank (MPI_COMM_WORLD, &id); 第一个是 Communicator第二个是进程 id 外部变量1234567int total;int main (int argc, char *argv[]) { int i; int id; int p; … 全局变量分配在静态存储区 工作循环分配12for (i = id; i < 65536; i += p) check_circuit (id, i); 外部功能检查电路具有并行性 它可以是一个普通、串行的函数 关闭 MPI1MPI_Finalize(); 在所有其他 MPI 库调用之后调用 允许系统释放 MPI 资源 完整代码编译 MPI 程序1mpicc -O -o foo foo.c mpicc:用于编译和链接 C+MPI 程序的脚本 -O 优化 -o 可执行文件存放位置 运行1mpirun -np <p> <exec> <arg1> … -np 进程数 可执行文件 命令参数 指定主机进程文件**.mpi-machines** 在home目录列出主机进程的使用顺序 例如:.mpi-machines 文件目录: band01.cs.ppu.eduband02.cs.ppu.eduband03.cs.ppu.eduband04.cs.ppu.edu 运行文件其对应的运行结果一个 CPU12345678910% mpirun -np 1 sat 0) 10101111100110010) 01101111100110010) 11101111100110010) 10101111110110010) 01101111110110010) 11101111110110010) 10101111101110010) 01101111101110010) 1110111110111001Process 0 is done 两个 CPU1234567891011% mpirun -np 2 sat 0) 01101111100110010) 01101111110110010) 01101111101110011) 10101111100110011) 11101111100110011) 10101111110110011) 11101111110110011) 10101111101110011) 1110111110111001Process 0 is doneProcess 1 is done 三个 CPU123456789101112% mpirun -np 3 sat 0) 01101111100110010) 11101111110110012) 10101111100110011) 11101111100110011) 10101111110110011) 01101111101110010) 10101111101110012) 01101111110110012) 1110111110111001Process 1 is doneProcess 2 is doneProcess 0 is done 输出分析 输出顺序仅部分反映并行计算机内部输出事件的顺序 如果进程 A 打印两条消息,则第一条消息将在第二条之前出现 如果进程 A 在进程 B 之前调用 printf,则不能保证进程 A 的消息会出现在进程 B 的消息之前 改进程序 希望找到解决方案的总数 将总和规约适用于这个程序 规约是一个集合的通信 修改 修改函数 check_circuit 如果电路满足输入组合,则返回 1 否则返回 0 每个进程保持它找到的可满足电路的本地计数 在 for 循环后执行归约 新的声明和代码1234567int count; /* Local sum */int global_count; /* Global sum */int check_circuit (int, int);count = 0;for (i = id; i < 65536; i += p) count += check_circuit (id, i); MPI_Reduce123456789101112131415int MPI_Reduce (void *operand, /* addr of 1st reduction element */ void *result, /* addr of 1st reduction result */ int count, /* reductions to perform */ MPI_Datatype type, /* type of elements */ MPI_Op operator, /* reduction operator */ int root, /* process getting result(s) */ MPI_Comm comm /* communicator */) MPI_DATATYPE OPTIONS1234567891011MPI_CHARMPI_DOUBLEMPI_FLOATMPI_INTMPI_LONGMPI_LONG_DOUBLEMPI_SHORTMPI_UNSIGNED_CHARMPI_UNSIGNEDMPI_UNSIGNED_LONGMPI_UNSIGNED_SHORT MPI_OP OPTIONS123456789101112MPI_BANDMPI_BORMPI_BXORMPI_LANDMPI_LORMPI_LXORMPI_MAXMPI_MAXLOCMPI_MINMPI_MINLOCMPI_PRODMPI_SUM 调用 MPI_Reduce12345678MPI_Reduce (&count, &global_count, 1, MPI_INT, MPI_SUM, 0,//only process 0 will get the result MPI_COMM_WORLD);if (!id) printf ("There are %d different solutions\\n",global_count); 第二个程序执行结果12345678910111213% mpirun -np 3 seq2 0) 01101111100110010) 11101111110110011) 11101111100110011) 10101111110110012) 10101111100110012) 01101111110110012) 11101111101110011) 01101111101110010) 1010111110111001Process 1 is doneProcess 2 is doneProcess 0 is doneThere are 9 different solutions MPI 基准测试程序 MPI_Barrier – barrier synchronization 障碍同步 MPI_Wtick – timer resolution 时钟分辨率 MPI_Wtime – current time 当前时间 wall-clock time VS CPU time(挂钟时间和 CPU 时间) CPU time (or process time) 是 CPU 用于处理指令的时间量。 用户时间+系统时间 Wall-clock time (or wall time ) 从开始到完成任务所经过的时间。 CPU 时间、I/O 时间和通信通道延迟 基准测试代码123double elapsed_time; …MPI_Init (&argc, &argv); MPI_Barrier (MPI_COMM_WORLD); elapsed_time = - MPI_Wtime(); …MPI_Reduce (…); elapsed_time += MPI_Wtime(); 结果 Processors Time (sec) 1 15.93 2 8.38 3 5.86 4 4.60 5 3.77","link":"/2021/06/03/%E5%88%86%E5%B8%83%E5%BC%8F/%E5%88%86%E5%B8%83%E5%BC%8Fchap-5/"},{"title":"分布式chap-6","text":"埃拉托斯特尼筛法串行算法 列出 2 以后的所有序列: 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 标出序列中的第一个质数,也就是 2,序列变成: 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 将剩下序列中,划摽 2 的倍数(用红色标出),序列变成: 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 如果现在这个序列中最大数小于等于最后一个标出的素数的平方,那么剩下的序列中所有的数都是质数,否则回到第二步。 时间复杂度:(nlnlnn) 并行性来源 区域划分 将数据分成几部分 将计算步骤和数据关联 每个数组元素一个原始任务 实现并行12345678910Making 3(A) parallelMark all multiples of k between k2 and n tofor all j where k2 j n do if j mod k = 0 then mark j (it is not a prime) endifendfor 123456789Making 3(B) parallelFind smallest unmarked number > ktoMin-reduction (to find smallest unmarked number > k)Broadcast (to get result to all tasks) 聚合的目标 合并任务 降低通信开销 平衡进程之间的计算 数据分解选项 交错(循环) 易于确定每个索引的“所有者” 导致此问题的负载不平衡 堵塞 平衡负载 易于标记倍数 如果 n 不是 p 的倍数,则确定所有者更复杂 块划分选项 想要在 n 不是 p 的倍数时平衡工作量 每个进程得到 n/p 上界 或 n/p 下界 元素 寻求简单的表达 查找给定所有者的低、高指数 给定索引查找所有者 方式 1 让 r = n mod p 如果 r = 0,则所有块的大小相同 别的 前 r 个块的大小为 n/p 上界 剩余的 p-r 个块的大小为 n/p 下界 方式 2 进程 i 的第一个元素 进程 i 的最后一个元素 给出元素 j,求对应的进程号 比较(数值为运算次数) Operations Method 1 Method 2 Low index 4 2 High index 6 4 Owner 7 4 运算次数最少的选择第二个方法 块分解宏命令1234567#define BLOCK_LOW(id,p,n) ((i)*(n)/(p))#define BLOCK_HIGH(id,p,n) \\ (BLOCK_LOW((id)+1,p,n)-1)#define BLOCK_SIZE(id,p,n) \\ (BLOCK_LOW((id)+1)-BLOCK_LOW(id))#define BLOCK_OWNER(index,p,n) \\ (((p)*(index)+1)-1)/(n)) 本地与全局索引 循环元素 分解影响实现 用于筛分的最大素数是$\\sqrt{n}$ 第一个进程有 n/p 下界 元素 如果 p < $\\sqrt{n}$,那么第一个进程包含可以筛掉所有非素数的素数 第一个进程总是广播下一个筛选素数 不需要规约过程 快速标记块分解允许与串行算法相同的标记: j, j + k, j + 2k, j + 3k, … 来代替 对于块中的所有 j如果 j mod k = 0 那么标记 j(它不是素数) 并行算法开发 广播函数 MPI_Bcast12345678int MPI_Bcast ( void *buffer, /* Addr of 1st element */ int count, /* # elements to broadcast */ MPI_Datatype datatype, /* Type of elements */ int root, /* ID of root process */ MPI_Comm comm) /* Communicator */MPI_Bcast (&k, 1, MPI_INT, 0, MPI_COMM_WORLD); 任务通道图 分析 $\\chi$是标记一个单元格所需的时间 串行执行时间是$\\chi$ n ln ln n 广播次数 :$\\sqrt{n}$ / $ln\\sqrt{n}$ 广播时间:$\\lambda$ 乘 $\\log$p 的上界 预计执行时间: 性能 执行串行算法确定 $\\chi$ = 85.47 纳秒 执行系列广播确定 $\\chi$ = 250 $\\mu$sec 不同进程数执行时间 Processors Predicted Actual (sec) 1 24.900 24.900 2 12.721 13.011 3 8.843 9.039 4 6.768 7.055 5 5.794 5.993 6 4.964 5.159 7 4.371 4.687 8 3.927 4.222 改进 删除偶数 将计算次数减半 较大的 n 值可以自由的存储 每个过程都能找到自己筛选的素数 将素数的计算复制到 $\\sqrt{n}$ 消除广播 重组循环 提高缓存命中率 优化比较","link":"/2021/06/05/%E5%88%86%E5%B8%83%E5%BC%8F/%E5%88%86%E5%B8%83%E5%BC%8Fchap-6/"},{"title":"leetcode 148 题 排序链表","text":"分治的思想,先分治,把数据结构不断对半分,直到最小单位,然后开始排序,再组合,递归实现。 这个题目是要把乱序的链表排成升序。由于链表的特殊结构,操作会有点复杂,不过也有比较好的方法。 分治 首先就是把大问题变成多个小问题,这里我们可以一直把链表对半分组。这里涉及 leetcode 876 题,找出链表的中间节点。用这个思路来把大问题化为多个小问题。 排列组合 第二件事就是排列组合了,把小问题排列组合成大一点的问题。这里涉及 leetcode 21 题 按顺序合并两个链表。 如此一来,一道中等难度的排序链表问题,能通过两道简单的链表查找和组合问题解决。 下面是代码: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768// Definition for singly-linked list.struct ListNode { int val; ListNode* next; ListNode() : val(0), next(nullptr) {} ListNode(int x) : val(x), next(nullptr) {} ListNode(int x, ListNode* next) : val(x), next(next) {}};class Solution { public: ListNode* sortList(ListNode* head) { // 递归 ListNode* sorted = mergeSort(head); return sorted; } ListNode* merge(ListNode* l1, ListNode* l2) { // 这是合并链表的 // 虚拟指针作头节点 // 这里不用new的方式创建dummy指针,后续提交能节省20MB左右的内存。 ListNode dummy(0); ListNode* tail = &dummy; // 开始合并 while (l1 && l2) { if (l1->val < l2->val) { tail->next = l1; l1 = l1->next; } else { tail->next = l2; l2 = l2->next; } tail = tail->next; } // 一个链表空了,把另外剩下的链表直接接上。 if (l1) { tail->next = l1; } else if (l2) { tail->next = l2; } // 返回虚拟节点的下一个节点作为头节点。 return dummy.next; } ListNode* mergeSort(ListNode* head) { // 这是快慢指针查找中间节点 if (!head || !head->next) { return head; } // 设定快慢指针 ListNode* slow = head; ListNode* fast = head->next; // 循环遍历 while (fast && fast->next) { slow = slow->next; fast = fast->next->next; } // 断开 ListNode* mid = slow->next; slow->next = NULL; // 断开后的两边链表再分别递归找中点然后断开。 ListNode* left = mergeSort(head); ListNode* right = mergeSort(mid); // 返回的两个链表按顺序拼接。 return merge(left, right); }}; 有个需要注意的点:mergeSort 这个函数是用来找中点的,其中快指针是 head->next 这是为了让找到的中点为偶数中间两个的前一个,如果只是 head 的话,将会是中间的后一个。如果是 876 题,是不能用这里的方式的,876 题求的是中间的后一个。","link":"/2023/03/01/%E5%8A%9B%E6%89%A3/leetcode%20148%20%E9%A2%98%20%E6%8E%92%E5%BA%8F%E9%93%BE%E8%A1%A8/"},{"title":"分布式chap-7","text":"弗洛伊德算法大纲 全对最短路径问题 动态二维数组 并行算法设计 点对点通信 块行矩阵 I/O 分析和基准测试 全对最短路径 算法1234567for k = 0 to n-1 for i = 0 to n-1 for j = 0 to n-1 a[i,j] = min (a[i,j], a[i,k] + a[k,j]) endfor endforendfor 动态二维数组动态一维数组建立 动态二维数组建立 分布式算法设计 划分 通信 聚合与映射 划分区域划分?功能划分? 看伪代码 执行相同的赋值语句 $n^3$ 次 没有并行性的函数 区域分解:将矩阵 A 划分为$n^2$个元素 每个 a[i,j]代表一项任务-需要找到一个最短的距离。 然而,要找到距离需要看看所有 k 的 a[i,k]和 a[k,j] 1代码同第一个代码块 通信 聚合与映射 任务数:静态 任务间的通信:结构化 每个人物的计算时间:恒定 计划 聚合任务以最大程度地减少通信 每个 MPI 进程创建一个任务 两种数据划分 比较逐列分块 消除了列内广播 逐行分块 消除行内广播 从文件中读取矩阵更简单 选择逐行分块如何输入一个邻接矩阵假设矩阵在文件中一排排地存储。方法 1) 每个进程都读取自己的行(或行)初始数据。该过程必须寻求正确的位置共享文件。方法 2) 主进程读取所有行并发送数据到适当的进程。 为什么我们不一次性输入整个文件,然后将其内容分散到各个进程中,以允许并发消息传递?(采用 MPI_ScatterMPI_Scatterv) MPI 组通信和点到点通信的一个重要区别,就在于它需要一个特定组内的所有进程同时参加通信而不是像点到点通信那样只涉及到发送方和接收方两个进程。组通信在各个不同进程的调用形式完全相同,而不像点到点通信那样在形式上就有发送和接收的区别。 组通信一般实现三个功能:通信、同步和计算。通信功能主要完成组内数据的传输,而同步功能实现组内所有进程在特定的地点在执行进度上取得一致,计算功能要对给定的数据完成一定的操作,例如加减等。所以 MPI_Send 会具有更好的效率 方法 2)最大限度地减少内存使用,因为只有一个进程需要读取和发送数据。消除了方法 1 中的寻求过程 )。 点对点通信 涉及一对进程 一个进程发送消息 其他进程收到消息 不集体发送接收数据 MPI_Send 函数12345678int MPI_Send ( void *message, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm) MPI_Recv 函数123456789int MPI_Recv ( void *message, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Status *status) MPI_Send 和 MPI_Recv 内部 MPI_Send 函数返回 函数直到消息缓冲区为空才会返回 以下情况缓存区空闲: 消息被复制到了系统缓冲区 消息被传输走了 典型场景 消息复制到系统缓冲区 传输重叠计算 MPI_Recv 函数返回 函数直到消息存在与缓冲区才返回 消息永远没到,则函数永远不返回 死锁死锁:进程等待一个永远不会成立的条件 编写简单的发送接收代码 两个进程:都先接收再发送 发送标签与接收标签不匹配 进程向错误的目标进程发送消息 例 1: 123456789if (id == 0) { MPI_Recv (&b,...); MPI_Send (&a,...); c = (a + b)/2.0;} else if (id == 1) { MPI_Recv (&a,...); MPI_Send (&b,...); c = (a + b)/2.0;} 进程 0 等待 1 的消息而阻塞,进程 1 等待 0 的消息而阻塞,产生了死锁。 例 2: 123456789if (id ==0) { MPI_Send(&a, ... 1,MPI_COMM_WORLD); MPI_Recv(&b, ... 1, MPI_COMM_WORLD,&status); c = (a+b)/2.0;}else if (id ==1) { MPI_Send(&a, ... 0,MPI_COMM_WORLD); MPI_Recv(&b, ... 0, MPI_COMM_WORLD,&status); c = (a+b)/2.0;} 这两个进程在尝试接收之前都会发送,但仍然陷入死锁。 为什么? 答:标签错了。进程 0 正在尝试接收 1 的标签,但进程 1 正在发送 0 的标签。 MPI 程序的安全 依赖 MPI 缓冲区的程序被认为是不安全的 这样的程序对于各种输入集可能运行没有问题,但它可能会挂起或崩溃与其他集。 MPI 标准允许 MPI_Send 以两种不同的方式运行: 他可以简单地将消息复制到 MPI 管理的缓冲区中并返回 他也可以阻塞,直到对 MPI_Recv 地匹配调用开始 MPI 的许多实现设置了系统从缓冲切换到阻塞的阈值 相对较小的消息将由 MPI_Send 缓冲 较大的消息将导致他阻塞 例 1:从进程 0 发送一个很大的消息到进程 1,如果目的地存储空间不足,发送必须等待用户提供内存空间(通过接收),下列执行顺序会发生什么? Process 0 Process 1 Send(1) Send(0) Recv(1) Recv(0) 这是“不安全”的,因为它依赖于系统缓冲区的可用性,需要 Process 0 暂时阻塞等待 Process 1 提供足够的内存。 例 2:生产者和消费者,如果进程 0 需要向处理 1 发送大量消息,并且发送速度比接受速度快。 Process 0 Process 1 Send(1) Recv(0) 在一开始的时候,系统的缓冲区并未被填满,后来满了之后,需要 Process 0 暂时阻塞等待 Process 1 提供足够的内存。 方法一:MPI_SsendMPI 标准定义的 MPI_Send 替代方法。额外的”s”表示同步,MPI_Ssend 保证阻塞,直到匹配接收开始。 12345678int MPI_Ssend(){ void * msg_buf_p; int msg_size; MPI_Datatype msg_type; int dest; int tag; MPI_Comm communicator;} 方法二:调整通信12345678910111213MPI_Send(msg,size,MPI_INT,(my_rank+1)%comm_sz,0,comm)MPI_Recv(new_msg,size,MPI_INT,(my_rank+comm_sz-1)%comm_sz,0,comm,MPI_STATUS_IGNORE) 改为if(my_rank%2==0){ MPI_Send(msg,size,MPI_INT,(my_rank+1)%comm_sz,0,comm); MPI_Recv(new_msg,size,MPI_INT,(my_rank+comm_sz-1)%comm_sz,0,comm,MPI_STATUS_IGNORE);}else{ MPI_Recv(new_msg,size,MPI_INT,(my_rank+comm_sz-1)%comm_sz,0,comm,MPI_STATUS_IGNORE); MPI_Send(msg,size,MPI_INT,(my_rank+1)%comm_sz,0,comm);} 偶数个 comm_sz 没问题,奇数个则不安全 方法三:MPI_Sendrecv 自己安排通信的替代方法。 在单个调用中执行阻塞发送和接收。 dest 和 source 可以相同或不同。 特别有用,因为 MPI 会安排通信以便程序不会挂起或崩溃。 1234567891011121314int MPI_Sendrecv( void* send_buf_p; int send_buf_size; MPI_Datatype send_buf_type; int dest; int send_tag; void* recv_buf_p; int recv_buf_size; MPI_Datatype recv_buf_type; int source; int recv_tag; MPI_Comm communicator; MPI_Status* status_p;); 集群通信和点对点通信 在通讯子中全部进程必须调用相同的集合函数 例如,如果一个程序试图将一个进程上对 MPI Reduce 的调用与另一个进程上对 MPI Recv 的调用相匹配,那么这个程序很可能会挂起或崩溃。 每个进程传递给 MPI 集体通信的参数必须是“兼容的”。 例如,如果一个进程传入 0 作为 目标进程 另一个传入 1,然后调用结果 MPI_Reduce 是错误的,而且程序很可能会挂起或崩溃。 out_data_p 参数只会被被 dest_process 使用 但是,所有的进程仍然需要传入一个对应的实参 out_data_p,即使它只是空值. 点对点通信在 标签和通信子的基础上被匹配。 集群通信不需要使用标签 集群通信不使用标签 他们被匹配仅仅在通信子的基础和他们被调用的顺序 块行剧真 I/O 并行代码123456789101112131415161718192021222324252627282930313233343536373839404142434445464748int main (int argc, char *argv[]){ dtype** a; /*doubly-subscripted array*/ dtype* storage; /*Loacl portion of array elements*/ int i,j,k; int id; int m;/*rows in maxtrix*/ int n;/*columns in maxtrix*/ int p;/*number of processes*/ double time,max_time; void compute_shortest_paths(int,int,int**,int); MPI_Init (&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD,&id); MPI_Comm_size(MPI_COMM_WORLD,&p); read_row_striped_matrix(argv[1],(void*) &a),(void*) &storage,MPI_TYPE,&m,&n,MPI_COMM_WORLD); if(m!=n) terminate(id,"Matrix must be square\\n"); pirnt_row_striped_matrix((void**) a,MPI_TYPE,0,MPI_COMM_WORLD); MPI_Barrier(MPI_COMMON_WORLD); time =-MPI_Wtime; compute_shortest_paths(id,p,(dtype**) a,n); time+=MPI_Wtime; MPI_Reduce(&time,&max_time,1,MPI_DOUBLE,MPI_MAX,0,MPI_COMM_WORLD); if(!id) printf("Floyd,matrix size %d,%d processes:%6.2f seconds\\n",n,p,max_time); pirnt_row_striped_matrix((void**) a,MPI_TYPE,0,MPI_COMM_WORLD); MPI_Finalize();}void compute_shortest_paths(int id,int p,dtype** a,int n){ int i,j,k; int offset;//local index of broadcast row int root;//process controlling row to be cast; int *tmp;//holds the broadcast row; tmp=(dtype *)malloc (n*sizeof(dtype)); for(k=0;k<n;k++){ root=BLOCK_OWNER(id,p,n); if(root==id){ offset=k-BLOCK_LOW(id,p,n); for(j=0;j<n;j++){ temp[j]=a[offset][l]; } MPI_Bcast(tmp,n,MPI_TYPE,root,MPI_COMM_WORLD); for(i=0;i<BLOCK_SIZE(id,p,n);i++){ for(j=0;j<n;j++){ a[i][j]=MIN(a[i][j],a[i][k]+temp[j]); } } } free (tmp); }} 分析和基准测试程序计算复杂度 最内层循环具有复杂性 Θ(n) 中间循环最多执行 ↑[n/p]次 外循环执行 n 次 总体复杂度 Θ(n3/p) 空间复杂度 内循环无通信 中间循环无通讯 外循环广播 — 复杂性是 Θ($nlogp$) 总体复杂度 ($n^2logp$) 执行表达式 表达式 1 计算/通信 重叠 表达式 2 预测与实际性能对比 Execution Time (sec) Processes Predicted Actual 1 25.54 25.54 2 13.02 13.89 3 9.01 9.60 4 6.89 7.29 5 5.86 5.99 6 5.01 5.16 7 4.40 4.50 8 3.94 3.98","link":"/2021/06/06/%E5%88%86%E5%B8%83%E5%BC%8F/%E5%88%86%E5%B8%83%E5%BC%8Fchap-7/"},{"title":"一些心里所想","text":"","link":"/2021/06/01/%E7%A7%98%E5%AF%86/%E4%B8%80%E4%BA%9B%E5%BF%83%E9%87%8C%E8%AF%9D/"},{"title":"FOMA PAN 100 - ROLL 1","text":"","link":"/2024/11/17/%E6%91%84%E5%BD%B1/FomaPan-100-roll-1/"}],"tags":[{"name":"C++","slug":"C","link":"/tags/C/"},{"name":"CUDA","slug":"CUDA","link":"/tags/CUDA/"},{"name":"Hexo","slug":"Hexo","link":"/tags/Hexo/"},{"name":"Blog","slug":"Blog","link":"/tags/Blog/"},{"name":"复习","slug":"复习","link":"/tags/%E5%A4%8D%E4%B9%A0/"},{"name":"分布式","slug":"分布式","link":"/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"},{"name":"素描","slug":"素描","link":"/tags/%E7%B4%A0%E6%8F%8F/"},{"name":"绘画","slug":"绘画","link":"/tags/%E7%BB%98%E7%94%BB/"},{"name":"力扣","slug":"力扣","link":"/tags/%E5%8A%9B%E6%89%A3/"},{"name":"排序算法","slug":"排序算法","link":"/tags/%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/"},{"name":"归并排序","slug":"归并排序","link":"/tags/%E5%BD%92%E5%B9%B6%E6%8E%92%E5%BA%8F/"},{"name":"链表","slug":"链表","link":"/tags/%E9%93%BE%E8%A1%A8/"},{"name":"摄影","slug":"摄影","link":"/tags/%E6%91%84%E5%BD%B1/"},{"name":"胶卷","slug":"胶卷","link":"/tags/%E8%83%B6%E5%8D%B7/"}],"categories":[{"name":"C++","slug":"C","link":"/categories/C/"},{"name":"Hexo Blog","slug":"Hexo-Blog","link":"/categories/Hexo-Blog/"},{"name":"分布式","slug":"分布式","link":"/categories/%E5%88%86%E5%B8%83%E5%BC%8F/"},{"name":"吃喝玩乐","slug":"吃喝玩乐","link":"/categories/%E5%90%83%E5%96%9D%E7%8E%A9%E4%B9%90/"},{"name":"力扣","slug":"力扣","link":"/categories/%E5%8A%9B%E6%89%A3/"},{"name":"摄影","slug":"摄影","link":"/categories/%E6%91%84%E5%BD%B1/"}],"pages":[{"title":"关于我","text":"","link":"/about/index.html"},{"title":"碎碎念","text":"tips:github 登录后按时间正序查看、可点赞加 ❤️、本插件地址 碎碎念加载中,请稍等... var gitalk = new Gitalk({ id: “d64e1b6440d8ef816848d79226db7cba”,repo: “self-talking”,owner: “Erial21”,clientID: “fff19f1ed037cc7e0d9c”,clientSecret: “c88201a14042a3ae4dc306cb616a5e35e953a2a5”,admin: [“Erial21”],createIssueManually: true,distractionFreeMode: false,perPage: 20,pagerDirection: “last”,enableHotKey: true,language: “zh-CN”,})gitalk.render(‘comment-container’)","link":"/self-talking/index.html"},{"title":"","text":"","link":"/FomaPan-100-roll-1/DSC0175jpg.html"}]}