OpenMP 框架是使用 C、C++ 和 Fortran 进行并发编程的一种强大方法。GNU Compiler Collection (GCC) V4.4.7 支持 OpenMP 3.0 标准,而 GCC 4.9.3 支持 OpenMP 4 标准。包括 VS 在内的其他编译器也支持 OpenMP。你可以学习使用 OpenMP 编译指示 (pragma),寻找对 OpenMP 提供的一些应用程序编程接口 (API) 的支持,并使用一些并行算法对 OpenMP 进行测试。本文将使用 GCC 5.4.0 作为首选编译器。

第一个 OpenMP 程序

入门:OpenMP 的一大特点就是您只需完成标准的 GCC 安装即可。支持 OpenMP 的程序必须使用 -fopenmp 选项进行编译。(也可以参考在VS中使用OpenMP)

让我们先从一个 Hello, World! 打印应用程序开始,它包括一个额外的编译指示.

清单1:使用了 OpenMP 的 Hello World 程序

1 #include <iostream>
2 #include<omp.h>
3 int main()
4 {
5   #pragma omp parallel
6   {
7     std::cout << "Hello World!\n";
8   }
9 }

在使用 g++ 编译和运行清单 1 中的代码时,控制台中应该会显示一个 Hello, World!。现在,使用 -fopenmp 选项重新编译代码。清单 2 显示了输出

清单 2. 使用 -fopenmp 命令编译并运行代码

1 tintin$ g++ test1.cpp -fopenmp
2 tintin$ ./a.out
3 Hello World!
4 Hello World!
5 Hello World!
6 Hello World!

发生了什么?#pragma omp parallel 仅在您指定了 -fopenmp 编译器选项后才会发挥作用。在编译期间,GCC 会根据硬件和操作系统配置在运行时生成代码,创建尽可能多的线程。每个线程的起始例程为代码块中位于指令之后的代码。这种行为是 隐式的并行化,而 OpenMP 本质上由一组功能强大的编译指示组成,帮您省去了编写大量样本文件的工作。(为了进行比较,您需要了解使用 Portable Operating System Interface (POSIX) 线程 [pthreads] 实现您刚才的程序将会怎样)。我这个测试是在虚拟机里进行的,使用的计算机运行的是 Intel® Core i5 处理器,分配有两个物理核心,每个核心有两个逻辑核心,因此清单 2 的输出看上去非常合理(4 个线程 = 4 个逻辑核心)。

接下来,让我们详细了解并行编译指示。

使用 OpenMP 实现并行处理的乐趣

使用编译命令的 num_threads 参数控制线程的数量非常简单。下面显示了 清单 1 中的代码,可用线程的数量被指定为 5(如 清单 3 所示)。

清单 3. 使用 num_threads 控制线程的数量
 1 #include <iostream>
 2 #include<omp.h>
 3
 4 int main()
 5 {
 6   #pragma omp parallel num_threads(5)
 7   {
 8     std::cout << "Hello World!\n";
 9   }
10 }

这里没有使用 num_threads 方法,而是使用另一种方法来修改运行代码的线程的数量。这还会将我们带到您将要使用的第一个 OpenMP API:omp_set_num_threads。在 omp.h 头文件中定义该函数。不需要链接到额外的库就可以获得 清单 4 中的代码,只需使用 -fopenmp

清单 4. 使用 omp_set_num_threads 对线程的创建进行调优
#include <iostream>
#include<omp.h>int main()
{omp_set_num_threads(5); #pragma omp parallel {std::cout << "Hello World!\n";}
}

最后,OpenMP 还使用了外部环境变量控制其行为。您可以调整 清单 2 中的代码,通过将 OMP_NUM_THREADS 变量设置为 6,就可以将 Hello World! 输出六次。清单     5 显示了代码的执行.

清单 5. 使用环境变量来调整 OpenMP 行为
1 tintin$ export OMP_NUM_THREADS=6
2 tintin$ ./a.out
3 Hello World!
4 Hello World!
5 Hello World!
6 Hello World!
7 Hello World!
8 Hello World!

目前,我们已经讨论了 OpenMP 的三个方面:编译指示、运行时 API 和环境变量。同时使用环境变量和运行时 API 会出现什么情况?运行时 API 将获得更高的优先权。

一个实际示例

OpenMP 使用隐式并行化技术,您可以使用编译指示、显式函数和环境变量来指导编译器的行为。让我们看一个 OpenMP 可以真正派上用场的示例。请考虑 清单 6 中的代码。
清单 6. 在 for 循环中执行顺序处理
1 int main( )
2 {
3 int a[1000000], b[1000000];
4 // ... some initialization code for populating arrays a and b;
5 int c[1000000];
6 for (int i = 0; i < 1000000; ++i)
7   c[i] = a[i] * b[i] + a[i-1] * b[i+1];
8 // ... now do some processing with array c
9  }

显然,您可以将 for 循环分为几个部分,在不同的核心中运行它们,任何 c[k] 与 c 数组中的其他元素都是独立的。清单 7 显示了 OpenMP 如何帮助您实现这一点。

 
清单 7. 在 for 循环中使用 parallel for 指令进行并行处理
 1 int main( )
 2 {
 3 int a[1000000], b[1000000];
 4 // ... some initialization code for populating arrays a and b;
 5 int c[1000000];
 6 #pragma omp parallel for
 7 for (int i = 0; i < 1000000; ++i)
 8   c[i] = a[i] * b[i] + a[i-1] * b[i+1];
 9 // ... now do some processing with array c
10  }

parallel for 编译指示可以帮助您将 for 循环工作负载划分到多个线程中,每个线程都可以在不同的核心上运行,这显著减少了总的计算时间。清单 8 演示了这一点。

清单 8. 理解 omp_get_wtime

 1 #include <omp.h>
 2 #include <math.h>
 3 #include <time.h>
 4 #include <iostream>
 5
 6 int main(int argc, char *argv[]) {
 7     int i, nthreads;
 8     clock_t clock_timer;
 9     double wall_timer;
10     double c[1000000];
11     for (nthreads = 1; nthreads <=8; ++nthreads) {
12         clock_timer = clock();
13         wall_timer = omp_get_wtime();
14         #pragma omp parallel for private(i) num_threads(nthreads)
15         for (i = 0; i < 1000000; i++)
16           c[i] = sqrt(i * 4 + i * 2 + i);
17         std::cout << "threads: " << nthreads <<  " time on clock(): " <<
18             (double) (clock() - clock_timer) / CLOCKS_PER_SEC
19            << " time on wall: " <<  omp_get_wtime() - wall_timer << "\n";
20     }
21 }

在 清单 8 中,可以通过不断增加线程的数量来计算运行内部 for 循环的时间。omp_get_wtime API 从一些任意的但是一致的点返回已用去的时间,以秒为单位。因此,omp_get_wtime() - wall_timer 将返回观察到的所用时间并运行 for 循环。clock() 系统调用用于预估整个程序的处理器使用时间,也就是说,将各个特定于线程的处理器使用时间相加,然后报告最终的结果。在我的 Intel Core i5 计算机中,清单 9 显示了最终的报告结果。

清单 9. 运行内部 for 循环的统计数据

1 threads: 1 time on clock(): 0.020236 time on wall: 0.0204209
2 threads: 2 time on clock(): 0.015448 time on wall: 0.00773731
3 threads: 3 time on clock(): 0.021125 time on wall: 0.00737248
4 threads: 4 time on clock(): 0.032429 time on wall: 0.00890261
5 threads: 5 time on clock(): 0.018109 time on wall: 0.00647498
6 threads: 6 time on clock(): 0.017804 time on wall: 0.00584579
7 threads: 7 time on clock(): 0.018956 time on wall: 0.00893618
8 threads: 8 time on clock(): 0.018512 time on wall: 0.00647849

尽管处理器时间在所有执行中都是相同的(应该是相同的,除去创建线程和上下文切换的时间),但是所用时间将随着线程数量的增加而逐渐减小,这意味着数据在核心中实现了并行处理。关于指令语法,最后一点需要注意的是:#pragma parallel for private(i) 意味着循环向量 i 将被作为一个线程本地存储进行处理,每个线程有一个该向量的副本。线程本地向量未进行初始化.

OpenMP 的临界区段(critical section)

您没有真正考虑过 OpenMP 是如何自动处理临界区段的,是吗?当然,您不需要显式创建一个互斥现象 (mutex),但是您仍然需要指定临界区段。下面显示了相关语法:

1 #pragma omp critical (optional section name)
2 {
3 // no 2 threads can execute this code block concurrently
4 }

pragma omp critical 之后的代码只能由一个线程在给定时间运行。同样,optional section name 是一个全局标识符,在同一时间,两个线程不能使用相同的全局标识符名称运行临界区段。请考虑 清单 10 中的代码。

清单 10. 多个具有相同名称的临界区段

1 #pragma omp critical (section1)
2 {
3 myhashtable.insert("key1", "value1");
4 }
5 // ... other code follows
6 #pragma omp critical (section1)
7 {
8 myhashtable.insert("key2", "value2");
9 }

在这一代码的基础上,您可以作出一个很安全的假设:永远不会出现两个散列表同时插入的情况,因为临界区段名是相同的。这与您在使用 pthread 时处理临界区段的方式有着明显的不同,后者的特点就是使用(或滥用)锁。

使用 OpenMP 实现锁和互斥

有趣的是,OpenMP 提供了自己的互斥(因此,它并不是全部关于编译指示):omp_lock_t,它被定义为 omp.h 头文件的一部分。常见的 pthread 形式的语法操作也适用,甚至 API 名称也是类似的。您需要了解以下 5 个 API:

  • omp_init_lock:此 API 必须是第一个访问 omp_lock_t 的 API,并且要使用它来完成初始化。注意,在完成初始化之后,锁被认为处于未设置状态。
  • omp_destroy_lock:此 API 会破坏锁。在调用该 API 时,锁必须处于未设置状态,这意味着您无法调用 omp_set_lock 并随后发出调用来破坏这个锁。
  • omp_set_lock:此 API 设置 omp_lock_t,也就是说,将会获得互斥。如果一个线程无法设置锁,那么它将继续等待,直到能够执行锁操作。
  • omp_test_lock:此 API 将在锁可用时尝试执行锁操作,并在获得成功后返回 1,否则返回 0。这是一个非阻塞 API, 也就是说,该函数不需要线程等待就可以设置锁。
  • omp_unset_lock:此 API 将会释放锁。

清单 11 显示了一个遗留的单线程队列,它被扩展为可使用 OpenMP 锁实现多线程处理。请注意,这一行为并不适合所有场景,这里主要用它来进行快速演示。

清单 11. 使用 OpenMP 扩展一个单线程队列

 1 #include <openmp.h>
 2 #include "myqueue.h"
 3
 4 class omp_q : public myqueue<int> {
 5 public:
 6    typedef myqueue<int> base;
 7    omp_q( ) {
 8       omp_init_lock(&lock);
 9    }
10    ~omp_q() {
11        omp_destroy_lock(&lock);
12    }
13    bool push(const int& value) {
14       omp_set_lock(&lock);
15       bool result = this->base::push(value);
16       omp_unset_lock(&lock);
17       return result;
18    }
19    bool trypush(const int& value)
20    {
21        bool result = omp_test_lock(&lock);
22        if (result) {
23           result = result && this->base::push(value);
24           omp_unset_lock(&lock);
25       }
26       return result;
27    }
28    // likewise for pop
29 private:
30    omp_lock_t lock;
31 };

嵌套锁

OpenMP 提供的其他类型的锁为 omp_nest_lock_t 锁的变体。它们与 omp_lock_t 类似,但是有一个额外的优势:已经持有锁的线程可以多次锁定这些锁。每当持有锁的线程使用 omp_set_nest_lock 重新获得嵌套锁时,内部计数器将会加一。当一个或多个对 omp_unset_nest_lock 的调用最终将这个内部锁计数器重置为 0 时,就会释放该锁。下面显示了用于 omp_nest_lock_t 的 API:

  • omp_init_nest_lock(omp_nest_lock_t* ):此 API 将内部嵌套计数初始化为 0
  • omp_destroy_nest_lock(omp_nest_lock_t* ):此 API 将破坏锁。使用非零内部嵌套计数对某个锁调用此 API 将会导致出现未定义的行为。
  • omp_set_nest_lock(omp_nest_lock_t* ):此 API 类似于 omp_set_lock,不同之处是线程可以在已持有锁的情况下多次调用这个函数。
  • omp_test_nest_lock(omp_nest_lock_t* ):此 API 是 omp_set_nest_lock 的非阻塞版本。
  • omp_unset_nest_lock(omp_nest_lock_t* ):此 API 将在内部计数器为 0 时释放锁。否则,计数器将在每次调用该方法时递减。

对任务执行的细粒度控制

在并行计算中,粒度是计算与通信之比的定性度量。最有效的粒度取决于算法及其运行的硬件环境,在大多数情况下,与通信和同步相关的开销相对于执行速度而言是高的,因此具有粗粒度是有利的;但细粒度并行可以帮助减少由于负载不平衡导致的开销。

我们已经了解了所有线程以并行的方式运行 pragma omp parallel 之后的代码块。您可以对代码块中的代码进一步分类,然后由选定的线程执行它。请考虑 清单 12 中的代码。

清单 12. 学习使用 parallel sections 编译指示

 1 #include<iostream>
 2 #include<omp.h>
 3 using namespace std;
 4
 5 int main( )
 6 {
 7   #pragma omp parallel num_threads(8)
 8   {
 9     cout << "All threads run this\n";
10     #pragma omp sections
11     {
12       #pragma omp section
13       {
14         cout << "This executes in parallel\n";
15       }
16       #pragma omp section
17       {
18         cout << "Sequential statement 1\n";
19         cout << "This always executes after statement 1\n";
20       }
21       #pragma omp section
22       {
23         cout << "This also executes in parallel\n";
24       }
25     }
26   }
27   return 0;
28 }

pragma omp sections 和 pragma omp parallel 之间的代码将由所有线程并行运行。pragma omp sections 之后的代码块通过 pragma omp section 进一步被分为各个子区段。每个 pragma omp section 块将由一个单独的线程执行。但是,区段块中的各个指令始终按顺序运行。清单 13 显示了清单 12 中代码的输出。

清单 13. 运行清单 12 中代码所产生的输出

 1 All threads run this
 2 This executes in parallel
 3 Sequential statement 1
 4 This always executes after statement 1
 5 This also executes in parallel
 6 All threads run this
 7 All threads run this
 8 All threads run this
 9 All threads run this
10 All threads run this
11 All threads run this
12 All threads run this

在清单 13 中,您将再次一开始就创建 8 个线程。对于这 8 个线程,只需使用三个线程执行 pragma omp sections 代码块中的工作。在第二个区段中,您指定了输出语句的运行顺序。这就是使用 sections 编译指示的全部意义。如果需要的话,您可以指定代码块的顺序。

理解与并行循环一起使用的 firstprivate 和 lastprivate 指令

在前文中,我们了解了如何使用 private 声明线程本地存储。那么,您应当如何初始化线程本地变量呢?在运行之前使用主线程中的变量的值同步本地变量?此时,firstprivate 指令就可以发挥作用了。

firstprivate 指令

使用 firstprivate(variable),您可以将线程中的变量初始化为它在主线程中的任意值。请参考 清单 14 中的代码。

清单 14. 使用与主线程不同步的线程本地变量
 1 #include <stdio.h>
 2 #include <omp.h>
 3
 4 int main()
 5 {
 6   int idx = 100;
 7   #pragma omp parallel private(idx)
 8   {
 9     printf("In thread %d idx = %d\n", omp_get_thread_num(), idx);
10   }
11 }

下面是我得到的输出。您的结果可能有所不同。

1 In thread 4 idx = 0
2 In thread 1 idx = 32660
3 In thread 2 idx = 0
4 In thread 3 idx = 0
5 In thread 5 idx = 0
6 In thread 6 idx = 0
7 In thread 0 idx = 0
8 In thread 7 idx = 0

清单 15 显示了带有 firstprivate 指令的代码。和期望的一样,输出在所有线程中将 idx 初始化为 100

清单 15. 使用 firstprivate 指令初始化线程本地变量

 1 #include <stdio.h>
 2 #include <omp.h>
 3
 4 int main()
 5 {
 6   int idx = 100;
 7   #pragma omp parallel firstprivate(idx)
 8   {
 9     printf("In thread %d idx = %d\n", omp_get_thread_num(), idx);
10   }
11 }

还要注意的是,您使用了 omp_get_thread_num( ) 方法访问线程的 ID。这与 Linux®top 命令显示的线程 ID 不同,并且这只是 OpenMP 用于跟踪线程数量的一种方式。如果您准备将 firstprivate 用于您的 C++ 代码,那么还要注意,firstprivate 指令使用的变量是一个副本构造函数,用于从主线程的变量初始化自身,因此对您的类使用一个私有的副本构造函数肯定会产生不好的结果。现在让我们了解一下lastprivate 指令,该指令在很多方面与 firstprivate 正好相反。

lastprivate 指令

与使用主线程的数据初始化线程本地变量不同,您现在将使用最后一次循环计数生成的数据同步主线程的数据。清单 16 中的代码运行了并行的 for 循环。

清单 16. 使用并行的 for 循环,没有与主线程进行同步

 1 #include <stdio.h>
 2 #include <omp.h>
 3
 4 int main()
 5 {
 6   int idx = 100;
 7   int main_var = 2120;
 8
 9   #pragma omp parallel for private(idx)
10   for (idx = 0; idx < 12; ++idx)
11   {
12     main_var = idx * idx;
13     printf("In thread %d idx = %d main_var = %d\n",
14       omp_get_thread_num(), idx, main_var);
15   }
16   printf("Back in main thread with main_var = %d\n", main_var);
17 }

在我的拥有 4 个核心的开发计算机上,OpenMP 为 parallel for 块创建了 3 个线程。每个线程执行两次循环迭代。main_var 的最终值取决于最后一个运行的线程,即该线程中的 idx 的值。换言之,main_var 的值并不取决于 idx 的最后一个值,而是任意一个最后运行的线程中的 idx 的值。清单 17 的代码解释了这一点。

清单 17. main_var 的值取决于最后一次线程运行

 1 In thread 2 idx = 6 main_var = 36
 2 In thread 2 idx = 7 main_var = 49
 3 In thread 2 idx = 8 main_var = 64
 4 In thread 0 idx = 0 main_var = 0
 5 In thread 0 idx = 1 main_var = 1
 6 In thread 0 idx = 2 main_var = 4
 7 In thread 3 idx = 9 main_var = 81
 8 In thread 3 idx = 10 main_var = 100
 9 In thread 3 idx = 11 main_var = 121
10 In thread 1 idx = 3 main_var = 9
11 In thread 1 idx = 4 main_var = 16
12 In thread 1 idx = 5 main_var = 25
13 Back in main thread with main_var = 25

多次运行清单 17 中的代码,确定主线程中的 main_var 的值始终依赖于最后运行的线程中的 idx 的值。那么如果您希望同步主线程的值与循环中 idx 的最终值,该怎样做呢?这就是 lastprivate 指令发挥其作用的地方,如 清单 18 所示。与清单 17 中的代码相似,多次运行清单 18 中的代码会发现主线程中的 main_var 的最终值为 121idx 为最终的循环计数器值)。

多次运行清单 17 中的代码,确定主线程中的 main_var 的值始终依赖于最后运行的线程中的 idx 的值。那么如果您希望同步主线程的值与循环中 idx 的最终值,该怎样做呢?这就是 lastprivate 指令发挥其作用的地方,如 清单 18 所示。与清单 17 中的代码相似,多次运行清单 18 中的代码会发现主线程中的 main_var 的最终值为 121idx 为最终的循环计数器值)。

清单 18. 使用 lastprivate 指令实现同步

 1 #include <stdio.h>
 2 #include <omp.h>
 3
 4 int main()
 5 {
 6   int idx = 100;
 7   int main_var = 2120;
 8
 9   #pragma omp parallel for private(idx) lastprivate(main_var)
10   for (idx = 0; idx < 12; ++idx)
11   {
12     main_var = idx * idx;
13     printf("In thread %d idx = %d main_var = %d\n",
14       omp_get_thread_num(), idx, main_var);
15   }
16   printf("Back in main thread with main_var = %d\n", main_var);
17 }

清单 19. 清单 18 的代码输出(请注意,主线程中 main_var always 值的等于 121)

In thread 1 idx = 3 main_var = 9
In thread 1 idx = 4 main_var = 16
In thread 1 idx = 5 main_var = 25
In thread 2 idx = 6 main_var = 36
In thread 2 idx = 7 main_var = 49
In thread 2 idx = 8 main_var = 64
In thread 0 idx = 0 main_var = 0
In thread 0 idx = 1 main_var = 1
In thread 0 idx = 2 main_var = 4
In thread 3 idx = 9 main_var = 81
In thread 3 idx = 10 main_var = 100
In thread 3 idx = 11 main_var = 121
Back in main thread with main_var = 121

最后一个注意事项:要支持对 C++ 对象使用 lastprivate 操作符,则需要相应的类具有公开可用的 operator= 方法。

使用 OpenMP 实现 merge sort

让我们看一个 OpenMP 将会帮助您节省运行时间的真实示例。这并不是一个对 merge sort 进行大量优化后的版本,但是足以显示在代码中使用 OpenMP 的好处。清单 20 显示了示例的代码。

清单 20. 使用 OpenMP 实现 merge sort

 1 #include <omp.h>
 2 #include <vector>
 3 #include <iostream>
 4 using namespace std;
 5
 6 vector<long> merge(const vector<long>& left, const vector<long>& right)
 7 {
 8     vector<long> result;
 9     unsigned left_it = 0, right_it = 0;
10
11     while(left_it < left.size() && right_it < right.size())
12     {
13         if(left[left_it] < right[right_it])
14         {
15             result.push_back(left[left_it]);
16             left_it++;
17         }
18         else
19         {
20             result.push_back(right[right_it]);
21             right_it++;
22         }
23     }
24
25     // Push the remaining data from both vectors onto the resultant
26     while(left_it < left.size())
27     {
28         result.push_back(left[left_it]);
29         left_it++;
30     }
31
32     while(right_it < right.size())
33     {
34         result.push_back(right[right_it]);
35         right_it++;
36     }
37
38     return result;
39 }
40
41 vector<long> mergesort(vector<long>& vec, int threads)
42 {
43     // Termination condition: List is completely sorted if it
44     // only contains a single element.
45     if(vec.size() == 1)
46     {
47         return vec;
48     }
49
50     // Determine the location of the middle element in the vector
51     std::vector<long>::iterator middle = vec.begin() + (vec.size() / 2);
52
53     vector<long> left(vec.begin(), middle);
54     vector<long> right(middle, vec.end());
55
56     // Perform a merge sort on the two smaller vectors
57
58     if (threads > 1)
59     {
60       #pragma omp parallel sections
61       {
62         #pragma omp section
63         {
64           left = mergesort(left, threads/2);
65         }
66         #pragma omp section
67         {
68           right = mergesort(right, threads - threads/2);
69         }
70       }
71     }
72     else
73     {
74       left = mergesort(left, 1);
75       right = mergesort(right, 1);
76     }
77
78     return merge(left, right);
79 }
80
81 int main()
82 {
83   vector<long> v(1000000);
84   for (long i=0; i<1000000; ++i)
85     v[i] = (i * i) % 1000000;
86   v = mergesort(v, 1);
87   for (long i=0; i<1000000; ++i)
88     cout << v[i] << "\n";
89 }

使用 8 个线程运行 merge sort 使运行时的执行时间变为 2.1 秒,而使用一个线程时,该时间为 3.7 秒。此处您惟一需要注意的是线程的数量。我使用了 8 个线程:具体的数量取决于系统的配置。但是,如果不明确指定的话,那么有可能会创建成千上百个线程,并且系统性能很可能会下降。前面讨论的 sections 编译指示可以很好地优化 merge sort 代码。

结束语

本文到此结束。我们在文章中介绍了大量内容:OpenMP 并行编译指示;学习了创建线程的不同方法;了解了 OpenMP 提供的更好的时间性能、同步和细粒度控制;并通过 merge sort 实际应用了 OpenMP。但是,有关 OpenMP 需要学习的内容还有很多,学习 OpenMP 的最好的地方就是 OpenMP 项目网站。请务必查看 参考资料 部分,获得有关的其他详细信息。

相关主题

  • 请务必访问 OpenMP 项目网站。
  • 有关提高 merge-sort 性能的更多信息,请参阅 Atanas Radenski 撰写的文章 Shared Memory, Message Passing, and Hybrid Merge Sorts for Standalone and Clustered SMPs。
参考链接:https://www.ibm.com/developerworks/cn/aix/library/au-aix-openmp-framework/index.html

转载于:https://www.cnblogs.com/lfri/p/10121712.html

通过 GCC 学习 OpenMP 框架相关推荐

  1. 如何自学php框架,如何学习php框架

    原标题:如何学习php框架 PHP作为网络开发的强大语言之一,现在应用非常广泛,主要运用于中小型企业.具有开放源代码,跨平台性强,开发快捷,效率高,面向对象,并且简单易学,容易于上手,并且是开源产品. ...

  2. 飞桨深度学习开源框架2.0抢先看:成熟完备的动态图开发模式

    百度飞桨于近期宣布,深度学习开源框架2.0抢先版本正式发布,进入2.0时代.其中一项重大升级,就是推出更加成熟完备的命令式编程模式,即通常说的动态图模式.同时在该版本中将默认的开发模式定为动态图模式, ...

  3. Karpathy更新深度学习开源框架排名:TensorFlow第一,PyTorch第二

    上周,Keras作者.谷歌研究科学家François Chollet晒出一张图,他使用Google Search Index,展示了过去三个月,ArXiv上提到的深度学习框架排行,新智元也做了报道: ...

  4. 人工智能深度学习Caffe框架介绍,优秀的深度学习架构

    在深度学习领域,Caffe框架是人们无法绕过的一座山.这不仅是因为它无论在结构.性能上,还是在代码质量上,都称得上一款十分出色的开源框架.更重要的是,它将深度学习的每一个细节都原原本本地展现出来,大大 ...

  5. 新手搭ssm要多久_如何快速学习ssm 框架?

    要快速学习SSM框架,你需要一套学习曲线平滑的教程 1. 很快可以看到效果 SSM框架这种教程的,在百度或者git上一搜一大把,不过很遗憾,大部分你照着上面的流程做,是做不出来的,要么缺少包,要么配置 ...

  6. 从Theano到Lasagne:基于Python的深度学习的框架和库

    从Theano到Lasagne:基于Python的深度学习的框架和库 [日期:2015-08-03] 来源:http://creative-punch.net/  作者:Creative Punch ...

  7. 斯坦福CS231n 2017最新课程:李飞飞详解深度学习的框架实现与对比

     斯坦福CS231n 2017最新课程:李飞飞详解深度学习的框架实现与对比 By ZhuZhiboSmith2017年6月19日 13:37 斯坦福大学的课程 CS231n (Convolutio ...

  8. TF学习——Tensorflow框架之基础概念、设计思路、常用方法之详细攻略

    TF学习--tensorflow框架之基础概念.设计思路.常用方法之详细攻略 目录 Tensorflow框架基础概念 Tensorflow框架设计思路 Tensorflow框架常用方法 Tensorf ...

  9. 【杂谈】超过12个,150页深度学习开源框架指导手册与GitHub项目,初学CV你值得拥有...

    之前我们公众号输出了很多深度学习开源框架相关的内容,今天整理成技术手册给大家分享以方便阅读,下面是详细信息. 开源框架背景 现如今开源生态非常完善,深度学习相关的开源框架众多,光是为人熟知的就有caf ...

最新文章

  1. linux 通过命令行获取本机外网IP
  2. wap问答系统工作总结
  3. ffmpeg + x264 编码H264(x86+arm)
  4. HDU - 4300 Clairewd’s message(扩展KMP)
  5. 数据库---初识sql语句
  6. php dom 丢失内容,PHP DOMDocument缺失
  7. 3.5 訪问者模式(5.11)
  8. django传递URL到某个app的urls.py文件中
  9. bzoj4498: 魔法的碰撞
  10. 173.二叉搜索树迭代器
  11. SQL SERVER2008查询分析器的使用
  12. 需求调研报告模板_培训需求调研报告不会写,遇到问题咋解决?
  13. 软件测试-黑盒测试:正交实验设计法
  14. Python——轮盘抽奖游戏
  15. 怎么选择靠谱Java培训机构?
  16. 结构化思维,让你的工作有条不紊
  17. opencv python 对指针仪表读数识别
  18. Centos 普通用户su到root用户提示错误
  19. uniapp处理tsconfig.json报错
  20. rtsp h256 流 web 软解播放

热门文章

  1. 抖音为何要自研芯片?
  2. MySQL入门实战 一
  3. 永磁直驱风力发电机并网仿真,机侧采用最大功率跟踪控制
  4. 最新全开源的个人主页源码/原创拟态UI3.0版
  5. leetcode-买卖股票/背包问题
  6. 配置小程序开发者工具及其使用(中)
  7. python中列表的元素是什么数据类型_python基本数据类型之列表和元组(一)
  8. Java速成学习小结
  9. 使用dom4j生成KML文件
  10. lambda表达式python菜鸟教程_[c#菜鸟]lambda表达式