OpenMP For Construct dynamic 调度方式实现原理和源码分析

news2025/1/18 17:04:31

OpenMP For Construct dynamic 调度方式实现原理和源码分析

前言

在本篇文章当中主要给大家介绍 OpenMp for construct 的实现原理,以及与他相关的动态库函数分析,与 for construct 非常相关的是循环的调度方式,在 OpenMP 当中一共有四种调调方式,auto, dynamic, guided, runtime, 在本篇文章当中主要是对 dynamic 的调度方式进行分析。

前置知识

在介绍 for construct 的实现原理之前,我们首先需要了解一下编译器是如何处理函数参数传递的(本文基于 x86_64 ISA),我们来看一下下面的代码在编译之后函数参数的传递情况。

在前面的文章当中我们已经谈到过了,在 x86 当中参数传递的规约,具体的内容如下所示:

寄存器含义
rdi第一个参数
rsi第二个参数
rdx第三个参数
rcx第四个参数
r8第五个参数
r9第六个参数

我们现在使用下面的代码来分析一下具体的情况(因为前面使用寄存器只能够传递 6 个参数,而在后面我们要分析的动态库函数当中会传递 7 个参数,因此这里我们使用 8 个参数来测试一下具体的参数传递情况):

#include "stdio.h"

void echo(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8)
{
  printf("%d %d %d %d %d %d %d %d\n", a8, a7, a1, a2, a3, a4, a5, a6);
}

int main()
{
  echo(1, 2, 3, 4 ,5 ,6, 7, 8);
  return 0;
}

上面的程序的反汇编结果如下所示:

000000000040053d <echo>:
  40053d:       55                      push   %rbp
  40053e:       48 89 e5                mov    %rsp,%rbp
  400541:       48 83 ec 30             sub    $0x30,%rsp
  400545:       89 7d fc                mov    %edi,-0x4(%rbp)
  400548:       89 75 f8                mov    %esi,-0x8(%rbp)
  40054b:       89 55 f4                mov    %edx,-0xc(%rbp)
  40054e:       89 4d f0                mov    %ecx,-0x10(%rbp)
  400551:       44 89 45 ec             mov    %r8d,-0x14(%rbp)
  400555:       44 89 4d e8             mov    %r9d,-0x18(%rbp)
  400559:       8b 7d f4                mov    -0xc(%rbp),%edi
  40055c:       8b 75 f8                mov    -0x8(%rbp),%esi
  40055f:       8b 55 fc                mov    -0x4(%rbp),%edx
  400562:       8b 45 18                mov    0x18(%rbp),%eax # a8
  400565:       8b 4d e8                mov    -0x18(%rbp),%ecx
  400568:       89 4c 24 10             mov    %ecx,0x10(%rsp)
  40056c:       8b 4d ec                mov    -0x14(%rbp),%ecx
  40056f:       89 4c 24 08             mov    %ecx,0x8(%rsp)
  400573:       8b 4d f0                mov    -0x10(%rbp),%ecx
  400576:       89 0c 24                mov    %ecx,(%rsp)
  400579:       41 89 f9                mov    %edi,%r9d
  40057c:       41 89 f0                mov    %esi,%r8d
  40057f:       89 d1                   mov    %edx,%ecx
  400581:       8b 55 10                mov    0x10(%rbp),%edx # a7
  400584:       89 c6                   mov    %eax,%esi # a8
  400586:       bf 64 06 40 00          mov    $0x400664,%edi
  40058b:       b8 00 00 00 00          mov    $0x0,%eax
  400590:       e8 8b fe ff ff          callq  400420 <printf@plt>
  400595:       c9                      leaveq 

0000000000400597 <main>:
  400597:       55                      push   %rbp
  400598:       48 89 e5                mov    %rsp,%rbp
  40059b:       48 83 ec 10             sub    $0x10,%rsp
  40059f:       c7 44 24 08 08 00 00    movl   $0x8,0x8(%rsp) # 保存参数 8 
  4005a6:       00 
  4005a7:       c7 04 24 07 00 00 00    movl   $0x7,(%rsp) # 保存参数 7 
  4005ae:       41 b9 06 00 00 00       mov    $0x6,%r9d # 保存参数 6 
  4005b4:       41 b8 05 00 00 00       mov    $0x5,%r8d # 保存参数 5 
  4005ba:       b9 04 00 00 00          mov    $0x4,%ecx # 保存参数 4 
  4005bf:       ba 03 00 00 00          mov    $0x3,%edx # 保存参数 3 
  4005c4:       be 02 00 00 00          mov    $0x2,%esi # 保存参数 2 
  4005c9:       bf 01 00 00 00          mov    $0x1,%edi # 保存参数 1
  4005ce:       e8 6a ff ff ff          callq  40053d <echo>
  4005d3:       b8 00 00 00 00          mov    $0x0,%eax
  4005d8:       c9                      leaveq 
  4005d9:       c3                      retq   
  4005da:       66 0f 1f 44 00 00       nopw   0x0(%rax,%rax,1)

从上面的汇编程序我们可以知道 1 - 6,这几个参数确实是通过寄存器传递的,对应的寄存器就是上文当中我们提到不同的参数对应的寄存器。但是参数 7 和参数 8 是保存在栈上的。根据上面的 main 函数的汇编程序分析,他对应的栈帧的内存布局如下所示:

我们在来分析一下 echo 函数当中 printf 函数参数的传递情况,第二个参数和第三个参数分别是 a8, a7,应该分别保存到寄存器 rsi/esi, rdx/edx 当中,在上面的汇编代码当中已经使用注释的方式进行标注出来了,从下往上进行分析可以看到 a8 保存在位置 0x18(%rbp),a7 保存在 0x10(%rbp),这个地址正是 main 函数保存 a7(当进入函数 echo 之后,a7,和 a8 的位置分别是 rsp + 0x10), a8(当进入函数 echo 之后,a7,和 a8 的位置分别是 rsp + 0x10 + 0x8) 的位置,具体可以结合上面的内存布局图进行分析。

dynamic 调度方式分析

我们使用下面的代码来分析一下动态调度的情况下整个程序的执行流程是怎么样的:

#pragma omp parallel for num_threads(t) schedule(dynamic, size)
for (i = lb; i <= ub; i++)
  body;

编译器会将上面的程序编译成下面的形式:

void subfunction (void *data)
{
  long _s0, _e0;
  while (GOMP_loop_dynamic_next (&_s0, &_e0))
  {
    long _e1 = _e0, i;
    for (i = _s0; i < _e1; i++)
      body;
  }
  // GOMP_loop_end_nowait 这个函数的主要作用就是释放数据的内存空间 在后文当中不进行分析
  GOMP_loop_end_nowait ();
}

GOMP_parallel_loop_dynamic_start (subfunction, NULL, t, lb, ub+1, 1, size);
subfunction (NULL);
// 这个函数在前面的很多文章已经分析过 本文也不在进行分析
GOMP_parallel_end ();
void
GOMP_parallel_loop_dynamic_start (void (*fn) (void *), void *data,
				  unsigned num_threads, long start, long end,
				  long incr, long chunk_size)
{
  gomp_parallel_loop_start (fn, data, num_threads, start, end, incr,
			    GFS_DYNAMIC, chunk_size);
}

static void
gomp_parallel_loop_start (void (*fn) (void *), void *data,
			  unsigned num_threads, long start, long end,
			  long incr, enum gomp_schedule_type sched,
			  long chunk_size)
{
  struct gomp_team *team;
  // 解析具体创建多少个线程
  num_threads = gomp_resolve_num_threads (num_threads, 0);
  // 创建一个含有 num_threads 个线程的线程组
  team = gomp_new_team (num_threads);
  // 对线程组的数据进行初始化操作
  gomp_loop_init (&team->work_shares[0], start, end, incr, sched, chunk_size);
  // 启动 num_threads 个线程执行函数 fn 
  gomp_team_start (fn, data, num_threads, team);
}

enum gomp_schedule_type
{
  GFS_RUNTIME, // runtime 调度方式
  GFS_STATIC,	 // static  调度方式
  GFS_DYNAMIC, // dynamic 调度方式
  GFS_GUIDED,	 // guided  调度方式
  GFS_AUTO     // auto    调度方式
};

在上面的程序当中 GOMP_parallel_loop_dynamic_start,有 7 个参数,我们接下来仔细解释一下这七个参数的含义:

  • fn,函数指针也就是并行域被编译之后的函数。
  • data,指向共享或者私有的数据,在并行域当中可能会使用外部的一些变量。
  • num_threads,并行域当中指定启动线程的个数。
  • start,for 循环迭代的初始值,比如 for(int i = 0; 😉 这个 start 就是 0 。
  • end,for 循环迭代的最终值,比如 for(int i = 0; i < 100; i++) 这个 end 就是 100 。
  • incr,这个值一般都是 1 或者 -1,如果是 for 循环是从小到达迭代这个值就是 1,反之就是 -1。
  • chunk_size,这个就是给一个线程划分块的时候一个块的大小,比如 schedule(dynamic, 1),这个 chunk_size 就等于 1 。

在函数 GOMP_parallel_loop_dynamic_start 当中会调用函数 gomp_parallel_loop_start ,这个函数的主要作用就是将整个循环的起始位置信息保存到线程组内部,那么就能够在函数 GOMP_loop_dynamic_next 当中直接使用这些信息进行不同线程的分块划分。GOMP_loop_dynamic_next 最终会调用函数 gomp_loop_dynamic_next ,其源代码如下所示:

static bool
gomp_loop_dynamic_next (long *istart, long *iend)
{
  bool ret;
  ret = gomp_iter_dynamic_next (istart, iend);
  return ret;
}

gomp_loop_dynamic_next 函数的返回值是一个布尔值:

  • 如果返回值为 true ,则说明还有剩余的分块需要执行。
  • 如果返回值为 false,则说明没有剩余的分块需要执行了,根据前面 dynamic 编译之后的结果,那么就会退出 while 循环。

gomp_iter_dynamic_next 是划分具体的分块,并且将分块的起始位置保存到变量 istart 和 iend 当中,因为传递的是指针,就能够使用 s0 和 e0 得到数据的值,下面是 gomp_iter_dynamic_next 的源代码,就是具体的划分算法了。

bool
gomp_iter_dynamic_next (long *pstart, long *pend)
{
  // 得到当前线程的指针
  struct gomp_thread *thr = gomp_thread ();
  // 得到线程组共享的数据
  struct gomp_work_share *ws = thr->ts.work_share;
  long start, end, nend, chunk, incr;
  
  // 保存迭代的最终值
  end = ws->end;
  // 这个值一般都是 1
  incr = ws->incr;
  // 保存分块的大小 chunk size
  chunk = ws->chunk_size;
  
  // ws->mode 在数据分块比较小的时候就是 1 在数据分块比较大的时候就是 0
  if (__builtin_expect (ws->mode, 1))
    {
    // __sync_fetch_and_add 函数是一个原子操作 ws->next 的初始值为 for 循环的起始位置值
    // 这个函数的返回值是 ws->next 的旧值 然后会将 ws->next 的值加上 chunk
    // 并且整个操作是原子的 是并发安全的
      long tmp = __sync_fetch_and_add (&ws->next, chunk);
    // 从小到大迭代
      if (incr > 0)
	{
	  if (tmp >= end)
	    return false;
    // 分块的最终位置
	  nend = tmp + chunk;
    // 溢出保护操作 分块的值需要小于最终的迭代位置
	  if (nend > end)
	    nend = end;
    // 将分块的值赋值给 pstart 和 pend 这样就能够在并行域当中得到这个分块的区间了
	  *pstart = tmp;
	  *pend = nend;
	  return true;
	}
      else
	{
    // 同样的原理不过是从大到小达迭代
	  if (tmp <= end)
	    return false;
	  nend = tmp + chunk;
	  if (nend < end)
	    nend = end;
	  *pstart = tmp;
	  *pend = nend;
	  return true;
	}
    }
  
  // 当数据分块比较大的时候执行下面的操作
  // 下面的整体的流程相对比较容易理解整个过程就是一个比较并交换的过程
  // 当比较并交换成功之后就返回结果 返回为 true 或者分块已经分完的话也进行返回
  start = ws->next;
  while (1)
    {
      long left = end - start;
      long tmp;
      // 如果分块已经完全分完 就直接返回 false 
      if (start == end)
	return false;

      if (incr < 0)
	{
	  if (chunk < left)
	    chunk = left;
	}
      else
	{
	  if (chunk > left)
	    chunk = left;
	}
      nend = start + chunk;

      tmp = __sync_val_compare_and_swap (&ws->next, start, nend);
      if (__builtin_expect (tmp == start, 1))
	break;

      start = tmp;
    }

  *pstart = start;
  *pend = nend;
  return true;
}

gomp_iter_dynamic_next 函数当中有两种情况的划分方式:

  • 当数据块相对比较小的时候,说明划分的次数就会相对多一点,在这种情况下如果使用 CAS 的话成功的概率就会相对低,对应的就会降低程序执行的效率,因此选择 __sync_fetch_and_add 以减少多线程的竞争情况,降低 CPU 的消耗。

  • 当数据块比较大的时候,说明划分的次数相对比较小,就使用比较并交换的操作(CAS),这样多个线程在进行竞争的时候开销就比较小。

在上面的文章当中我们提到了,gomp_loop_init 函数是对线程共享数据 work_share 进行初始化操作,如果你对具体 work_share 中的数据初始化规则感兴趣,下面是对其初始化的程序:

static inline void
gomp_loop_init (struct gomp_work_share *ws, long start, long end, long incr,
		enum gomp_schedule_type sched, long chunk_size)
{
  ws->sched = sched;
  ws->chunk_size = chunk_size;
  /* Canonicalize loops that have zero iterations to ->next == ->end.  */
  ws->end = ((incr > 0 && start > end) || (incr < 0 && start < end))
	    ? start : end;
  ws->incr = incr;
  ws->next = start;
  if (sched == GFS_DYNAMIC)
    {
      ws->chunk_size *= incr;

#ifdef HAVE_SYNC_BUILTINS
      {
	/* For dynamic scheduling prepare things to make each iteration
	   faster.  */
	struct gomp_thread *thr = gomp_thread ();
	struct gomp_team *team = thr->ts.team;
	long nthreads = team ? team->nthreads : 1;

	if (__builtin_expect (incr > 0, 1))
	  {
	    /* Cheap overflow protection.  */
	    if (__builtin_expect ((nthreads | ws->chunk_size)
				  >= 1UL << (sizeof (long)
					     * __CHAR_BIT__ / 2 - 1), 0))
	      ws->mode = 0;
	    else
	      ws->mode = ws->end < (LONG_MAX
				    - (nthreads + 1) * ws->chunk_size);
	  }
	/* Cheap overflow protection.  */
	else if (__builtin_expect ((nthreads | -ws->chunk_size)
				   >= 1UL << (sizeof (long)
					      * __CHAR_BIT__ / 2 - 1), 0))
	  ws->mode = 0;
	else
	  ws->mode = ws->end > (nthreads + 1) * -ws->chunk_size - LONG_MAX;
      }
#endif
    }
}

实例分析

在本小节当中我们将使用一个实际的例子去分析上面我们所谈到的整个过程:

#include <stdio.h>
#include <omp.h>

int main()
{
#pragma omp parallel for num_threads(4) default(none) schedule(dynamic, 2)
  for(int i = 0; i < 12; ++i)
  {
    printf("i = %d tid = %d\n", i, omp_get_thread_num());
  }
  return 0;
}

上面的程序被编译之后的结果如下所示,具体的程序分析和注释都在下面的汇编程序当中:

000000000040073d <main>:
  40073d:       55                      push   %rbp
  40073e:       48 89 e5                mov    %rsp,%rbp
  400741:       48 83 ec 20             sub    $0x20,%rsp
  400745:       48 c7 04 24 02 00 00    movq   $0x2,(%rsp) # 这个就是 chunk size 符合上面的代码当中指定的 2
  40074c:       00 
  40074d:       41 b9 01 00 00 00       mov    $0x1,%r9d # 因为是从小到达 incr 这个参数是 1
  400753:       41 b8 0c 00 00 00       mov    $0xc,%r8d # 这个参数是 end 符合上面的程序 12
  400759:       b9 00 00 00 00          mov    $0x0,%ecx # 这个参数是 start 符合上面的程序 1
  40075e:       ba 04 00 00 00          mov    $0x4,%edx # num_threads(4) 线程的个数是 4
  400763:       be 00 00 00 00          mov    $0x0,%esi # 因为上面的代码当中并没有在并行域当中使用数据 因此这个数据为 0 也就是 NULL 
  400768:       bf 88 07 40 00          mov    $0x400788,%edi # 函数指针 main._omp_fn.0
  40076d:       e8 ce fe ff ff          callq  400640 <GOMP_parallel_loop_dynamic_start@plt>
  400772:       bf 00 00 00 00          mov    $0x0,%edi
  400777:       e8 0c 00 00 00          callq  400788 <main._omp_fn.0>
  40077c:       e8 5f fe ff ff          callq  4005e0 <GOMP_parallel_end@plt>
  400781:       b8 00 00 00 00          mov    $0x0,%eax
  400786:       c9                      leaveq 
  400787:       c3                      retq
  
0000000000400788 <main._omp_fn.0>:
  400788:       55                      push   %rbp
  400789:       48 89 e5                mov    %rsp,%rbp
  40078c:       53                      push   %rbx
  40078d:       48 83 ec 38             sub    $0x38,%rsp
  400791:       48 89 7d c8             mov    %rdi,-0x38(%rbp)
  400795:       c7 45 ec 00 00 00 00    movl   $0x0,-0x14(%rbp)
  40079c:       48 8d 55 e0             lea    -0x20(%rbp),%rdx
  4007a0:       48 8d 45 d8             lea    -0x28(%rbp),%rax
  4007a4:       48 89 d6                mov    %rdx,%rsi
  4007a7:       48 89 c7                mov    %rax,%rdi
  4007aa:       e8 21 fe ff ff          callq  4005d0 <GOMP_loop_dynamic_next@plt>
  4007af:       84 c0                   test   %al,%al # 如果 GOMP_loop_dynamic_next 返回值是 0 则跳转到 4007fb 执行函数 GOMP_loop_end_nowait
  4007b1:       74 48                   je     4007fb <main._omp_fn.0+0x73>
  4007b3:       48 8b 45 d8             mov    -0x28(%rbp),%rax
  4007b7:       89 45 ec                mov    %eax,-0x14(%rbp)
  4007ba:       48 8b 45 e0             mov    -0x20(%rbp),%rax
  4007be:       89 c3                   mov    %eax,%ebx
  # ===========================下面的代码就是执行循环和 body =================
  4007c0:       e8 2b fe ff ff          callq  4005f0 <omp_get_thread_num@plt>
  4007c5:       89 c2                   mov    %eax,%edx
  4007c7:       8b 45 ec                mov    -0x14(%rbp),%eax
  4007ca:       89 c6                   mov    %eax,%esi
  4007cc:       bf 94 08 40 00          mov    $0x400894,%edi
  4007d1:       b8 00 00 00 00          mov    $0x0,%eax
  4007d6:       e8 25 fe ff ff          callq  400600 <printf@plt>
  4007db:       83 45 ec 01             addl   $0x1,-0x14(%rbp)
  4007df:       39 5d ec                cmp    %ebx,-0x14(%rbp)
  4007e2:       7c dc                   jl     4007c0 <main._omp_fn.0+0x38>
  # ======================================================================
  # ============下面的代码主要是进行 while 循环查看循环是否执行完成==============
  4007e4:       48 8d 55 e0             lea    -0x20(%rbp),%rdx
  4007e8:       48 8d 45 d8             lea    -0x28(%rbp),%rax
  4007ec:       48 89 d6                mov    %rdx,%rsi
  4007ef:       48 89 c7                mov    %rax,%rdi
  4007f2:       e8 d9 fd ff ff          callq  4005d0 <GOMP_loop_dynamic_next@plt>
  4007f7:       84 c0                   test   %al,%al
  4007f9:       75 b8                   jne    4007b3 <main._omp_fn.0+0x2b>
  # ======================================================================
  4007fb:       e8 10 fe ff ff          callq  400610 <GOMP_loop_end_nowait@plt>
  400800:       48 83 c4 38             add    $0x38,%rsp
  400804:       5b                      pop    %rbx
  400805:       5d                      pop    %rbp
  400806:       c3                      retq   
  400807:       66 0f 1f 84 00 00 00    nopw   0x0(%rax,%rax,1)
  40080e:       00 00

总结

在本篇文章当中我们主要分析了 OpenMP 当中 for 循环动态调度方式的具体实现原理,以及动态库函数的分析。整个过程主要有两大部分,一个是编译角度,编译器会将 for construct 编译成什么样子,以及动态库函数具体是如何划分迭代分块的。在迭代分块当中主要分为两种方式,当分块数目多的时候不使用 CAS 因为这样线程之间竞争比较激烈,但是当分块数目比较小的时候就使用 CAS ,这种做法可以提高程序执行的效率。


更多精彩内容合集可访问项目:https://github.com/Chang-LeHung/CSCore

关注公众号:一无是处的研究僧,了解更多计算机(Java、Python、计算机系统基础、算法与数据结构)知识。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/197730.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

KVM,QEMU与libvirt关系

KVM&#xff1a;负责cpu虚拟化内存虚拟化&#xff0c;实现了cpu和内存的虚拟化&#xff0c;但kvm不能模拟其他设备&#xff1b;KVM是linux内核的模块&#xff0c;它需要CPU的支持&#xff0c;采用硬件辅助虚拟化技术Intel-VT&#xff0c;AMD-V&#xff0c;内存的相关如Intel的E…

FPGA纯verilog代码实现8位精简指令集CPU,一学期的微机原理不如看懂这套代码,提供工程源码和技术支持

目录1、前言2、设计思想和架构3、硬件组成讲解4、vivado仿真5、vivado工程6、上板调试验证7、福利&#xff1a;工程源码获取1、前言 本文章主要针对大学本科阶段学生&#xff1b; 读文章之前先来几个灵魂拷问&#xff1a; 1、你是否学过《微机原理》、《单片机》、《汇编语言》…

怎样做一个优秀的程序员?这10个问题ChatGPT这样说 ……

本文目录 1 怎样做一个优秀的程序员? 2 怎样成为优秀的架构师? 3 怎样写容易阅读的代码? 4 怎样做项目管理? 5 怎样学习计算机程序设计? 6 怎样提升个人影响力? 7 怎样提升认知? 8 程序员怎样面试通过几率高? 9 怎样提升研发效能? 10 怎样保障软件系统的稳定…

字体图标的使用【购物车】

方法1 <link rel"stylesheet" href"2购物车/iconfont.css"><style>*{padding: 0;margin: 0;}li{width: 90px;height: 40px;background-color: pink;margin: 0 auto; list-style: none;text-align: center;line-height: 40px;}a{text-decoratio…

4.6 Python元组

列表非常适合用于存储在程序运行期间可能变化的数据集。Python将不能修改的值称为不可变的&#xff0c;而不可变的列表被称为元组。4.5.1 定义元组元组看起来犹如列表&#xff0c;但使用圆括号而不是方括号来标识。定义元组后&#xff0c;就可以使用索引来访问其元素&#xff0…

vue3组件库项目学习笔记(七):正式开发问题拾遗

目前组件库的开发还在进行中&#xff0c;这里把一些开发过程中遇到的问题和解决方案总结一下&#xff0c;也方便后续开发的同学踩坑了可以马上解决问题&#xff0c;这期的问题主要是&#xff1a; 多项目开发的配置文件优化和全局导入方法的修改怎么样使用 icon 组件怎么样使用…

git-学习git,这一篇就足够了(初学者视角实战教程)

目录git概念命令git配置README.gitignore工作区、暂存区和版本库基础配置创建远程仓库克隆修改查看工作区当前状态添加到暂存区回退版本比较工作区与缓存区的差异添加到本地仓库并加注释push提高git pull文件删除与恢复分支管理列出分支创建分支切换分支分支操作标签管理创建标…

【linux】进程间通信——管道通信

进程间通信一、进程间通信1.1 通信的介绍1.2 通信的目的1.3 通信的分类二、管道2.1 匿名管道2.1.1 pipe2.2.2 读写特征2.2.3 命名管道一、进程间通信 1.1 通信的介绍 通信就是一个进程把数据传递给另一个进程&#xff0c;但是每个进程都具有独立性。通信的本质&#xff1a;OS需…

STL——vector

一、标准库中的vector 1.vector文档介绍 &#xff08;1&#xff09;vector是表示可变大小数组的序列容器。 &#xff08;2&#xff09;像数组一样&#xff0c;vector也采用连续存储空间来存储元素&#xff0c;也就意味着可以采用下标对vector的元素进行访问&#xff0c;和数…

深度:用10000字总结了嵌入式C语言必学知识点

导读&#xff1a;怎么做好嵌入式&#xff1f;相信这个问题无论问谁你都会得到一句学好C语言&#xff01;今天推荐一篇大佬写的嵌入式C语言知识点总结&#xff0c;非常值得一读。 目录 1 关键字 2 数据类型 3 内存管理和存储架构 4 指针和数组 5 结构类型和对齐 6 预处理…

RDC 2022纪念版开发板-D1S在RT-Smart运行

开发环境 软件 ubuntu20.04VMware Workstation 硬件 RDC2022纪念版开发板全志D1s芯片 材料下载 首先打开虚拟机&#xff0c;创建一个目录存放本次测试的代码&#xff0c;然后克隆RT-Smart用户态代码。 git clone https://github.com/RT-Thread/userapps.git在userapps目…

SMB2协议特性之oplock与lease(下

前期回顾上篇文章我们介绍了oplock/lease的相关概念及其基本工作原理&#xff0c;由于间隔时间较长&#xff0c;忘记的读者可以先去回顾一下。本篇文章带大家了解一下&#xff0c;在实际场景中&#xff0c;oplock/lease是如何工作的。实际场景分析在一些警匪影视剧中&#xff0…

PCI驱动程序框架

PCI驱动程序框架 文章目录PCI驱动程序框架参考资料&#xff1a;一、 PCI驱动框架二、 RK3399驱动致谢参考资料&#xff1a; 《PCI Express Technology 3.0》&#xff0c;Mike Jackson, Ravi Budruk; MindShare, Inc.《PCIe扫盲系列博文》&#xff0c;作者Felix&#xff0c;这是…

【NS2】打印c++函数名字/bash将echo赋值给变量

需求&#xff1a;将tcl在c调用的路由算法名字&#xff08;函数名&#xff09;输出&#xff0c;并作为变量赋值给文件名字&#xff0c;但就怎么将函数名字打印出来就思考了很久&#xff0c;并尝试了其他网站“在shell脚本使用tcl变量、如何在bash脚本打印tcl变量、NS2&#xff0…

【实际开发12】- 经验 experience

目录 1. 经验 experience 1. 无多大价值 , 停留数据展示层面 2. 保证数据一致性问题 3. 新增时 , 可先关注核心基础数据 ( 复杂数据以修改形式完善 ) 4. 新增 / 修改 ( 幂等性处理 ) 5. 增 / 删 / 改 添加日志 , 查询无需日志 6. 需要对接多模块的通用字段设计 : String…

什么是CRM系统 企业如何选择合适的CRM系统

在如今市场竞争激烈情况下&#xff0c;企业更加注重客户的数据和管理&#xff0c;因此逐渐形成了“以客户为核心”的理念。而借助CRM系统管理客户数据已然成为一种趋势。 选择一款适合企业的CRM系统可以帮助企业实现更多的价值。但一些企业在初期根本不了解什么是CRM系统&…

Hadoop安装(一) --- JDK安装

目录 1.安装虚拟机 2.关防火墙 3.修改网络配置文件 4.重启网络服务 5.连接shell 6.安装vim工具 7.免密登陆 8. 开启远程免密登录配置 9.远程登录 10.同步时间 10.1.安装ntpdate 10.2.定时更新时间 10.3.启动定时任务 10.4.查看状态 11.修改计算机名 12.配置ho…

数据仓库的架构以及传统数据库与数据仓库的区别

一、数据仓库的分层架构 数据仓库的数据来源于不同的源数据&#xff0c;并提供多样的数据应用&#xff0c;数据自下而上流入数据仓库后向上层开放应用&#xff0c;而数据仓库只是中间集成化数据管理的一个平台。 1&#xff0c;源数据层&#xff08;ODS&#xff09; 操作性数…

袁树雄和杨语莲究竟什么关系 ,《早安隆回》走红后又是《汉川》

自从《早安隆回》火爆全网后&#xff0c;歌迷们就有一种担心&#xff0c;不知道这首好听的歌曲&#xff0c;究竟还能再够火爆多久。歌迷们的担心也不无道理&#xff0c;毕竟花无百日红&#xff0c;人无千般好&#xff0c;《早安隆回》就是再好听&#xff0c;也不可能红一辈子吧…

windows搭建go语言开发环境

1.下载Go语言开发包可以在Go语言官网 ( https://golang.google.cn/dl/ )下载Windows 系统下的Go语言开发包&#xff0c;如下图所示。这里我下载的是 64位的开发包&#xff0c;如果读者的电脑是 32 位系统的话&#xff0c;则需要下载 32 位的开发包&#xff0c;在上图所示页面中…