• [其他干货] CUDA编程(十)TOP K的解题思路
    Day4课后作业如下:其中第一题,在上面的Day4链接中,张小白已经做了。那么第二题怎么做呢?老师提供了一个函数是给top k个字段排序的:__device__ __host__ void insert_value(int* array, int k, int data) { for (int i = 0; i < k; i++) { if (array[i] == data) { return; } } if (data < array[k - 1]) return; for (int i = k - 2; i >= 0; i--) { if (data > array[i]) array[i + 1] = array[i]; else { array[i + 1] = data; return; } } array[0] = data; }我们求解top10的思路是什么呢?当然仍然是延续这个万能的框架。我们来看下求最大值和最小值的框架,只留下最大值的部分:2_1.cu#include<stdio.h> #include<stdint.h> #include<time.h> //for time() #include<stdlib.h> //for srand()/rand() #include<sys/time.h> //for gettimeofday()/struct timeval #include"error.cuh" #define N 10000000 #define BLOCK_SIZE 256 #define BLOCKS ((N + BLOCK_SIZE - 1) / BLOCK_SIZE) __managed__ int source[N]; //input data __managed__ int final_result[2] = {INT_MIN,INT_MAX}; //scalar output __global__ void _sum_min_or_max(int *input, int count,int *output) { __shared__ int max_per_block[BLOCK_SIZE]; int max_temp = INT_MIN; for (int idx = threadIdx.x + blockDim.x * blockIdx.x; idx < count; idx += gridDim.x * blockDim.x ) { max_temp = (input[idx] > max_temp) ? input[idx] :max_temp; } max_per_block[threadIdx.x] = max_temp; //the per-thread partial max is temp! __syncthreads(); //**********shared memory summation stage*********** for (int length = BLOCK_SIZE / 2; length >= 1; length /= 2) { int max_double_kill = -1; if (threadIdx.x < length) { max_double_kill = (max_per_block[threadIdx.x] > max_per_block[threadIdx.x + length]) ? max_per_block[threadIdx.x] : max_per_block[threadIdx.x + length]; } __syncthreads(); //why we need two __syncthreads() here, and, if (threadIdx.x < length) { max_per_block[threadIdx.x] = max_double_kill; } __syncthreads(); //....here ? } //the per-block partial sum is sum_per_block[0] if (blockDim.x * blockIdx.x < count) //in case that our users are naughty { //the final reduction performed by atomicAdd() if (threadIdx.x == 0) atomicMax(&output[0], max_per_block[0]); } } int _max_min_cpu(int *ptr, int count, int *max1, int *min1) { int max = INT_MIN; for (int i = 0; i < count; i++) { max = (ptr[i] > max)? ptr[i]:max; } //printf(" CPU max = %d\n", max); *max1 = max; return 0; } void _init(int *ptr, int count) { uint32_t seed = (uint32_t)time(NULL); //make huan happy srand(seed); //reseeding the random generator //filling the buffer with random data for (int i = 0; i < count; i++) { //ptr[i] = rand() % 100000000; ptr[i] = rand() ; if (i % 2 == 0) ptr[i] = 0 - ptr[i] ; } } double get_time() { struct timeval tv; gettimeofday(&tv, NULL); return ((double)tv.tv_usec * 0.000001 + tv.tv_sec); } int main() { //********************************** fprintf(stderr, "filling the buffer with %d elements...\n", N); _init(source, N); //********************************** //Now we are going to kick start your kernel. cudaDeviceSynchronize(); //steady! ready! go! fprintf(stderr, "Running on GPU...\n"); double t0 = get_time(); _sum_min_or_max<<<BLOCKS, BLOCK_SIZE>>>(source, N,final_result); CHECK(cudaGetLastError()); //checking for launch failures CHECK(cudaDeviceSynchronize()); //checking for run-time failures double t1 = get_time(); fprintf(stderr, " GPU max: %d\n", final_result[0]); //********************************** //Now we are going to exercise your CPU... fprintf(stderr, "Running on CPU...\n"); double t2 = get_time(); int cpu_max=0; int cpu_min=0; int B = _max_min_cpu(source, N, &cpu_max, &cpu_min); printf(" CPU max = %d\n", cpu_max); printf(" CPU min = %d\n", cpu_min); double t3 = get_time(); //fprintf(stderr, "CPU sum: %u\n", B); //******The last judgement********** //if ( final_result_max == cpu_max && final_result_min == cpu_min ) if ( final_result[0] == cpu_max ) { fprintf(stderr, "Test Passed!\n"); } else { fprintf(stderr, "Test failed!\n"); exit(-1); } //****and some timing details******* fprintf(stderr, "GPU time %.3f ms\n", (t1 - t0) * 1000.0); fprintf(stderr, "CPU time %.3f ms\n", (t3 - t2) * 1000.0); return 0; } 编译运行:那么,我们继续在这个框架的基础上,把计算top 10的部分加上去。该怎么加呢?显然的,需要把上面计算max的部分全部换成计算top10的部分:我们看到上面两个定义:__shared__ int max_per_block[BLOCK_SIZE]; int max_temp =0;max_per_block是存放最大值的,现在要存放topk(k=10)个最大值,所以显然我们需要将max_per_block[BLOCK_SIZE]扩容成 max_per_block[BLOCK_SIZE* topk],为了对比方便,将max_per_block改为 topk_per_block:同理,将max_temp扩充为 topk_temp[topk];第2个地方:根据 inut[idx]计算出 topk_temp: max_temp = (input[idx] > max_temp) ? input[idx] :max_temp;直接改为insert_value(topk_temp, TOPK, input[idx]);第3个地方:根据topk_temp 计算出 topk_per_block[ threadIdx.x * TOPK ]到 topk_per_block[ threadIdx.x * TOPK+TOPK-1 ] :max_per_block[threadIdx.x] = max_temp; //the per-thread partial max is temp!改为:for(int i = 0; i< TOPK ; i++) { topk_per_block[ threadIdx.x * TOPK + i] = topk_temp[i]; }第4个地方: max_double_kill = (max_per_block[threadIdx.x] > max_per_block[threadIdx.x + length]) ? max_per_block[threadIdx.x] : max_per_block[threadIdx.x + length];这里原来是取 max_per_block[threadIdx.x] 和 max_per_block[threadIdx.x + length]) 间的最大值,同样换成insert_value函数: for(int i=0;i<TOPK ;i++) { insert_value(topk_temp, TOPK , topk_per_block[ (threadIdx.x + length) * TOPK + i]); }第5个地方: max_per_block[threadIdx.x] = max_double_kill;改为:for(int i=0;i<TOPK ;i++) { topk_per_block[threadIdx.x *TOPK + i]= topk_temp[i]; }第6个地方:if (threadIdx.x == 0) atomicMax(&output[0], max_per_block[0]);改为: for(int i=0;i<TOPK ;i++) { output[TOPK * blockIdx.x +i ] = topk_per_block[i]; }注:这里可以更简单的改为:if (threadIdx.x < TOPK) output[TOPK * blockIdx.x + threadIdx.x ] = topk_per_block[threadIdx.x];这样直接整体并行写入即可,而且还是合并的。核函数改完之后,调用核函数的地方也做以下改动:_sum_min_or_max<<<BLOCKS, BLOCK_SIZE>>>(source, N,final_result);改为_sum_min_or_max<<<BLOCKS, BLOCK_SIZE>>>(source, N, _1pass_results); _sum_min_or_max<<<1, BLOCK_SIZE>>>(_1pass_results, TOPK * BLOCKS, final_result);这里需要解释一下,为啥原来取最大值的时候调用一次核函数就行了,但是取TOPK就需要调用2次呢?因为并没有一个同时处理TOPK个元素替换的原子操作(但是有很多替换1个元素的原子操作)当然,比较CPU和GPU的地方也做相应的改动(这点看下面的代码就行了)修改完的代码如下:2_1.cu#include<stdio.h> #include<stdint.h> #include<time.h> //for time() #include<stdlib.h> //for srand()/rand() #include<sys/time.h> //for gettimeofday()/struct timeval #include"error.cuh" #define N 10000000 #define BLOCK_SIZE 256 #define BLOCKS ((N + BLOCK_SIZE - 1) / BLOCK_SIZE) #define TOPK 10 __managed__ int source[N]; //input data __managed__ int final_result[TOPK] = {INT_MIN}; //scalar output __managed__ int _1pass_results[TOPK * BLOCKS]; __device__ __host__ void insert_value(int* array, int k, int data) { for (int i = 0; i < k; i++) { if (array[i] == data) { return; } } if (data < array[k - 1]) return; for (int i = k - 2; i >= 0; i--) { if (data > array[i]) array[i + 1] = array[i]; else { array[i + 1] = data; return; } } array[0] = data; } __global__ void _sum_min_or_max(int *input, int count,int *output) { //__shared__ int max_per_block[BLOCK_SIZE]; __shared__ int topk_per_block[BLOCK_SIZE * TOPK]; //int max_temp = INT_MIN; int topk_temp [TOPK]; for(int i=0;i<TOPK;i++) topk_temp[i] = INT_MIN; for (int idx = threadIdx.x + blockDim.x * blockIdx.x; idx < count; idx += gridDim.x * blockDim.x ) { //max_temp = (input[idx] > max_temp) ? input[idx] :max_temp; insert_value(topk_temp, TOPK, input[idx]); } //max_per_block[threadIdx.x] = max_temp; //the per-thread partial max is temp! for(int i = 0; i< TOPK ; i++) { topk_per_block[ threadIdx.x * TOPK + i] = topk_temp[i]; } __syncthreads(); //**********shared memory summation stage*********** for (int length = BLOCK_SIZE / 2; length >= 1; length /= 2) { //int max_double_kill = -1; if (threadIdx.x < length) { //max_double_kill = (max_per_block[threadIdx.x] > max_per_block[threadIdx.x + length]) ? max_per_block[threadIdx.x] : max_per_block[threadIdx.x + length]; for(int i=0;i<TOPK ;i++) { insert_value(topk_temp, TOPK , topk_per_block[ (threadIdx.x + length) * TOPK + i]); } } __syncthreads(); //why we need two __syncthreads() here, and, if (threadIdx.x < length) { //max_per_block[threadIdx.x] = max_double_kill; for(int i=0;i<TOPK ;i++) { topk_per_block[threadIdx.x * TOPK + i]= topk_temp[i]; } } __syncthreads(); //....here ? } //the per-block partial sum is sum_per_block[0] if (blockDim.x * blockIdx.x < count) //in case that our users are naughty { //the final reduction performed by atomicAdd() // if (threadIdx.x == 0) atomicMax(&output[0], max_per_block[0]); if (threadIdx.x < TOPK) output[TOPK * blockIdx.x + threadIdx.x ] = topk_per_block[threadIdx.x]; /* for(int i=0;i<TOPK ;i++) { output[TOPK * blockIdx.x +i ] = topk_per_block[i]; } */ } } int _max_min_cpu(int *ptr, int count, int *max1, int *min1) { int max = INT_MIN; for (int i = 0; i < count; i++) { max = (ptr[i] > max)? ptr[i]:max; } //printf(" CPU max = %d\n", max); *max1 = max; return 0; } void cpu_result_topk(int* input, int count, int* output) { /*for (int i = 0; i < TOPK; i++) { output[i] = INT_MIN; }*/ for (int i = 0; i < count; i++) { insert_value(output, TOPK, input[i]); } } void _init(int *ptr, int count) { uint32_t seed = (uint32_t)time(NULL); //make huan happy srand(seed); //reseeding the random generator //filling the buffer with random data for (int i = 0; i < count; i++) { //ptr[i] = rand() % 100000000; ptr[i] = rand() ; if (i % 2 == 0) ptr[i] = 0 - ptr[i] ; } } double get_time() { struct timeval tv; gettimeofday(&tv, NULL); return ((double)tv.tv_usec * 0.000001 + tv.tv_sec); } int main() { cudaEvent_t start, stop; CHECK(cudaEventCreate(&start)); CHECK(cudaEventCreate(&stop)); //********************************** fprintf(stderr, "filling the buffer with %d elements...\n", N); _init(source, N); //********************************** //Now we are going to kick start your kernel. CHECK(cudaEventRecord(start)); cudaDeviceSynchronize(); //steady! ready! go! fprintf(stderr, "Running on GPU...\n"); double t0 = get_time(); // _sum_min_or_max<<<BLOCKS, BLOCK_SIZE>>>(source, N,final_result); _sum_min_or_max<<<BLOCKS, BLOCK_SIZE>>>(source, N, _1pass_results); CHECK(cudaGetLastError()); //checking for launch failures _sum_min_or_max<<<1, BLOCK_SIZE>>>(_1pass_results, TOPK * BLOCKS, final_result); CHECK(cudaGetLastError()); //checking for launch failures CHECK(cudaDeviceSynchronize()); //checking for run-time failures CHECK(cudaEventRecord(stop)); CHECK(cudaEventSynchronize(stop)); double t1 = get_time(); for(int i=0;i<TOPK;i++) fprintf(stderr, " GPU max[%d]: %d\n", i,final_result[i]); //********************************** //Now we are going to exercise your CPU... fprintf(stderr, "Running on CPU...\n"); double t2 = get_time(); int cpu_result[TOPK] = { 0 }; //int cpu_max=0; //int cpu_min=0; //int B = _max_min_cpu(source, N, &cpu_max, &cpu_min); cpu_result_topk(source, N, cpu_result); //printf(" CPU max = %d\n", cpu_max); double t3 = get_time(); //fprintf(stderr, "CPU sum: %u\n", B); int ok = 1; for (int i = 0; i < TOPK; ++i) { printf("cpu top%d: %d; gpu top%d: %d \n", i + 1, cpu_result[i], i + 1, final_result[i]); if (fabs(cpu_result[i] - final_result[i]) > (1.0e-10)) { ok = 0; } } if (ok) { printf("Pass!!!\n"); } else { printf("Error!!!\n"); } //******The last judgement********** /* //if ( final_result_max == cpu_max && final_result_min == cpu_min ) if ( final_result[0] == cpu_max ) { fprintf(stderr, "Test Passed!\n"); } else { fprintf(stderr, "Test failed!\n"); exit(-1); } */ //****and some timing details******* fprintf(stderr, "GPU time %.3f ms\n", (t1 - t0) * 1000.0); fprintf(stderr, "CPU time %.3f ms\n", (t3 - t2) * 1000.0); return 0; } 我们来运行一下:这样下去,算top5,top20,top50应该都是可以的吧?top5:top20:top50:LOL,张小白想得太美好了~~只好改为top40看看:貌似算得有点慢了,但是还能出个结果:那到底有什么好的计算方式呢?还有,现有方式还能提速吗?这个萧敬腾的天气,又给张小白创造了好几个难题。。。。看来还得好好学习啊。。。另外,张小白忘记自己还有个Jetson AGX Orin了。让我们看看它能不能突破下极限:仍然用top40计算。确实比Nano快很多(但是仍然跑不过CPU)改成top50:额,还是编译不过去。4G内存和32G内存的设备,看来shared memory是一样大的??张小白默默看了下定义:__shared__ int topk_per_block[BLOCK_SIZE * TOPK];当然是的。一个block最多能用48kB。也就是说,如果BLOCK_SIZE设置成前面代码中的256的话,那么TOPK为50的时候,256X50X4已经超过48K了。(1个int占用4个字节)。所以樊老师说了,BLOCK_SIZE=256的时候,TOPK最大能到48。我们试试:#define BLOCK_SIZE 256 #define BLOCKS ((N + BLOCK_SIZE - 1) / BLOCK_SIZE) #define TOPK 48Nano的表现:改成TOP49,果然不可以编译:那只有一种办法了,就是降低BLOCK_SIZE,比如说改为128。根据前面的算法,128X4X96等于48K。以此类推,可以算到64,32时候的TOPN最大数量。我们也就不一一截图了,直接用表格填入结果:只贴一个:表格如下:TOPNBLOCK_SIZENano CPU(ms)NanoGPU(ms)Orin CPU(ms)Orin GPU(ms)5256433.401252.809131.36644.42010256107.692777.333240.60499.99520256476.2213414.480511.257256.92740256765.03629736.0221079.4761576.12648256845.73540406.8321259.630224.73250256编译错编译错50128882.79934380.9851355.7411512.643100641575.11394527.5262709.5051940.573961281513138183.3922576.2145307.14497128编译错编译错192642831.961653679.9355193.0016091.51119364编译错编译错38432太长不算了太长不算了70072.33210363.46638532编译错编译错48128859.61832778.1531293.6521194.0834864853.53421058.5781293.964926.6994832845.07015701.8021292.892997.095注:上述结果仅为一次测量结果。不排除多次测量会出现抖动或者差异很大的情况。以上的结果确认了几个事情:1.共享内存最大确实只有48K,多一毛都没有。想挤牙膏很难。2.目前的这种reduce算法还是存在很大的局限性的,它在TOPN较小的时候较为高效。3.对于TOPN较大,还不如直接调用cublas或者thrust做完全排序(不过这个张小白因为不考试——所以没好好学。。LOL。。下次补上)4.减小BLOCKSIZE确实可以计算,但是BLOCKSIZE越小,SN占有率就越小。一个SM最多可以驻留2048(或者少一点)的线程,在BLOCKSIZE=128时,占有率为 6.25%;BLOCKSIZE=64时,占有率为 3.125%;BLOCKSIZE=32时,占有率为 1.5625%。从上面的结果也可以看出,BLOCKSIZE变化确实会引起性能较大的变化。如TOP48:Orin从BLOCK 256-》128-》64》32分别是 224ms-》1194ms-》926ms-》997ms。后面几个差距不大(因为存在预热),但是256到128发生巨变,说明最佳值在256这里。Nano从BLOCK 256-》128-》64》32分别是40s-》32s-》32s-》15s。反而是BLOCK越小速度越快。当然这个并不能说明有这个正比关系。只能说明Nano设备并不是运行TOP48的最佳机选。所以,下次考试,如果可以换成Orin集群。。那大家考试将会多爽啊~~~在最终开发CUDA程序的时候,是从整个程序的角度发力,如果一个地方并不是关键的,那没有必要优化到极致。用什么算法都可以。先应该花力气解决最关键的部分。注:往TOPK个元素中插入TOPK个元素,并最终保留TOPK个元素(就是只留下TOPK个元素),如果使用插入法,时间复杂度为O(n^2)的。随着K的扩大,比如从10个变成100个的情况下,算法时间的变大将是灾难性的。这点其实在张小白的测试中也可以略微看出。其实有训练营的童鞋提出了线性的解决方案,比如双指针法,又比如bucket法,可以将两组TOPK个元素组合成1组按高低排序的K个元素,这个时候的算法时间复杂度是O(n)。另外,针对随机数本身的分布特性,还可以快速求出TOPK。这点,张小白只好留做一个问题,将来再研究了。。。(未完待续)
  • [技术干货] 编译器工程师眼中的好代码(1):Loop Interchange
    > 编者按:C/C++代码在编译时,编译器将源码翻译成CPU可识别的指令序列并生成可执行代码,而最终代码的运行效率取决于由编译器生成的可执行代码。在大部分情况下,编写源代码时就已经决定了程序可以在何种程度下被编译器优化。即使对源代码做微小改动也可能会对编译器生成的代码运行效率产生重大影响。因此,源代码的优化可以在一定程度上帮助编译器生成更高效的可执行代码。 > > 本文将以Loop Interchange的场景为例,讲述在编写代码时可以拿到更优性能的书写方式。 ## 1. Loop Interchange 相关基本概念 ### 1.1 访问局部性 访问局部性指的是在计算机科学领域中应用程序在访问内存的时候,倾向于访问内存中较为靠近的值。这种局部性是出现在计算机系统中的一种可预测行为,我们可以利用系统的这种强访问局部性来进行性能优化。访问局部性分为三种基本形式,分别为时间局部性、空间局部性、循序局部性。 而本文主要讲述的Loop Interchange主要是利用了空间局部性。空间局部性指的是,最近引用过的内存位置以及其周边的内存位置容易再次被使用。比较常见于循环中,比如在一个数组中,如果第3个元素在上一个循环中被使用,那么本次循环中极有可能会使用第4个元素;如果本次循环确实使用第4个元素,就是命中上一次迭代所prefetch到的cache数据。 所以对于数组循环运算,可以利用空间局部性这一特征,保证两次相邻循环中对数组元素的访问在内存上是更加靠近的,即循环访问数组中的元素时stride越小,相应的性能可能会有所优化。 那么,数组在内存上是如何存储的呢? ### 1.2 Row-major 和 Column-major Row-major 和 Column-major 是两种将多维数组存储在线性存储中的方式。数组的元素在内存中是连续的;Row-major ordering代表行的连续元素在内存中彼此相邻,而Column-major ordering则是代表列的连续元素彼此相邻,如下图所示。 虽然Row和Column的名称看起来像是特指二维数组,但是Row-major和Column-major也可以推广适用于任何维度的数组。 那么在C/C++中,数组是以以上哪种方式存储的呢? 举一个小例子,用cachegrind工具来展示C使用两种不同的访问形式的CPU的cache丢失率对比。 按行访问: ``` #include int main(){ size_t i,j; const size_t dim = 1024 ; int matrix [dim][dim]; for (i=0;i dim; i++) for (j=0;j return 0; } ``` 按列访问: ``` #include int main(){ size_t i,j; const size_t dim = 1024 ; int matrix [dim][dim]; for (i=0;i dim; i++){ for (j=0;j } } return 0; } ``` 根据上述C代码中对相同数组的两种不同访问方式时cache丢失率的对比,可以说明在C/C++代码中,数组是以Row-major形式储存的。也就是说,如果前一步访问了`a[i][j]`,那么对`a[i][j+1]`的访问会命中cache。这样就不会执行对主存储器的访问,而cache比主内存快,因此遵循相应编程语言的储存形式使其可以命中cache可能会带来优化。 至于其他常用的编程语言,Fortran、MATLAB等则是默认Column-major形式。 ### 1.3 Loop Interchange Loop Interchange利用系统倾向于访问内存中较为靠近的值的特征以及C/C++ Row-major的特点,通过改变循环嵌套中两个循环之间的执行顺序,增加整体代码空间局部性。此外,它还可以启用其他重要的代码转换,例如,Loop Reordering就是Loop Interchange扩展到两个以上循环被重新排序时的优化。在LLVM中,Loop Interchange需要通过使能`-mllvm -enable-loopinterchange`选项启用。 ## 2. 优化示例 ### 2.1 基础场景 简单看下面一个矩阵运算的示例: 原始代码: ``` for(int i = 0; i 2048; i++) { for(int j = 0; j 1024; j++) { for(int k = 0; k 1024; k++) { C[i * 1024 + j] += A[i * 1024 + k] * B[k * 1024 + j]; } } } ``` 试着把`jk`两层循环进行Loop Interchange之后的代码: ``` for(int i = 0; i 2048; i++) { for(int k = 0; k 1024; k++) { for(int j = 0; j 1024; j++) { C[i * 1024 + j] += A[i * 1024 + k] * B[k * 1024 + j]; } } } ``` 可以发现, 在原始代码中,最内层的`k`每次迭代,`C`要访问的数据不变,`A`每次访问的stride为1,大概率命中cache,但`B`由于每次访问的stride为1024,几乎每次都会cache miss。 Loop Interchange之后,`j`位于最内层循环,每次迭代时`A`每次要访问的数据不变,`C`和`B`每次访问的stride为1,都会有很大概率命中cache,cache命中率大大增加。 那么cache命中率是否真的增加,以及两者的性能又如何呢? 原始代码: ``` $ time -p ./a.out ``` ``` $ sudo perf stat -r 3 -e cache-misses,cache-references,L1-dcache-load-misses,L1-dcache-loads ./a.out ``` Loop Interchange后的结果如下: ``` $ time -p ./a.out ``` ``` $ sudo perf stat -r 3 -e cache-misses,cache-references,L1-dcache-load-misses,L1-dcache-loads ./a.out ``` 两者相比: L1-dcache-loads的数目差不多,因为要访问的数据总量差不多; L1-dcache-load-misses所占L1-dcache-loads的比例在进行loop interchange代码修改后降低了将近10倍。 同时,性能数据上也能带来接近9.5%的性能提升。 ### 2.2 特殊场景 当然,在实际使用时,并不是所有的场景都是如2.1中所示的那种工整的可Loop Interchange场景。 ``` for ( int i = 0; i N; ++ i ) { if( I[i] != 1 ) continue; for ( int m = 0; m M; ++ m ) { Res2 = res[m][i] * res[m][i]; norm[m] += Res2; } } ``` 如上述场景,如果N是超大数组,那么Loop Interchange理论上可以带来较大收益;但由于两层循环中间增加一个分支判断,导致原本可以Loop Interchange的场景无法实现。 针对这种场景,可以考虑将中间的分支判断逻辑剥离,可以先保证Loop Interchange使得数组`res`在连续内存上进行访问;至于中间的判断分支逻辑,可以在Loop Interchange两层循环后再进行回退。 ``` for ( int m = 0; m M; ++ m ) { for ( int i = 0; i N; ++ i) { Res2 = res[m][i] * res[m][i]; norm[m] += Res2; if( I[i] != 1 ) //补充逻辑,保证源代码语义 { norm[m] -= Res2; continue; } } } ``` 当然,这样的源码修改也需要考虑cost是否值得,如果该if分支进入频率非常高,那么之后回退带来的cost也会较大,可能就需要重新考虑Loop Interchange是否值得;反之,如果分支进入频率非常低,那么Loop Interchange的实现还是可以带来可观的收益的。 ## 3. 毕昇编译器对Loop Interchange pass社区的贡献 毕昇编译器团队在llvm社区中对Loop Interchange pass也做出了不小的贡献。团队从legality、profitability等方面对Loop Interchange pass做了全方位的增强,同时也对该pass所支持的场景做了大量的扩展。在Loop Interchange方面,近两年来团队小伙伴为社区提供了二十余个主要的patch,包含Loop Interchange,以及相关的dependence analysis、loop cache analysis、delinearization等分析和优化的增强。简单举几个例子: - D114916 [LoopInterchange] Enable loop interchange with multiple outer loop indvars (https://reviews.llvm.org/D114916) - D114917 [LoopInterchange] Enable loop interchange with multiple inner loop indvars (https://reviews.llvm.org/D1149167) 这两个patch将Loop Interchange的应用场景扩展到内层或者外层循环中包含不止一个induction variable的情况: ``` for (c = 0, e = 1; c + e 150; c++, e++) { d = 5; for (; d; d--) a |= b[d + e][c + 9]; } } ``` - D118073 [IVDescriptor] Get the exact FP instruction that does not allow reordering (https://reviews.llvm.org/D118073) - D117450 [LoopInterchange] Support loop interchange with floating point reductions (https://reviews.llvm.org/D117450) 这两个patch将Loop Interchange的应用场景扩展到支持浮点类型的reduction计算的场景: ``` double matrix[dim][dim]; for (i=0;i dim; i++) for (j=0;j ``` D120386 [LoopInterchange] Try to achieve the most optimal access pattern after interchange (https://reviews.llvm.org/D120386) 这个patch增强了Interchange的能力使编译器能够将循环体permute成为全局最优的循环顺序: ``` void f(int e[100][100][100], int f[100][100][100]) { for (int a = 0; a 100; a++) { for (int b = 0; b 100; b++) { for (int c = 0; c 100; c++) { f[c][b][a] = e[c][b][a]; } } } } ``` => ``` void f(int e[100][100][100], int f[100][100][100]) { for (int c = 0; c 100; c++) { for (int b = 0; b 100; b++) { for (int a = 0; a 100; a++) { f[c][b][a] = e[c][b][a]; } } } } ``` D124926 [LoopInterchange] New cost model for loop interchange (https://reviews.llvm.org/D124926) 这个patch为loop interchange提供了一个全新的,功能更强的cost model,可以更准确地对loop interchange的profitability做出判断。 此外,我们还为社区提供了大量的bugfix的patch: - D102300 [LoopInterchange] Check lcssa phis in the inner latch in scenarios of multi-level nested loops - D101305 [LoopInterchange] Fix legality for triangular loops - D100792 [LoopInterchange] Handle lcssa PHIs with multiple predecessors - D98263 [LoopInterchange] fix tightlyNested() in LoopInterchange legality - D98475 [LoopInterchange] Fix transformation bugs in loop interchange - D102743 [LoopInterchange] Handle movement of reduction phis appropriately during transformation (pr43326 && pr48212) - D128877 [LoopCacheAnalysis] Fix a type mismatch bug in cost calculation 以及其他功能的增强: - D115238 [LoopInterchange] Remove a limitation in legality - D118102 [LoopInterchange] Detect output dependency of a store instruction with itself - D123559 [DA] Refactor with a better API - D122776 [NFC][LoopCacheAnalysis] Add a motivating test case for improved loop cache analysis cost calculation - D124984 [NFC][LoopCacheAnalysis] Update test cases to make sure the outputs follow the right order - D124725 [NFC][LoopCacheAnalysis] Use stable_sort() to avoid non-deterministic print output - D127342 [TargetTransformInfo] Added an option for the cache line size - D124745 [Delinearization] Refactoring of fixed-size array delinearization - D122857 [LoopCacheAnalysis] Enable delinearization of fixed sized arrays ## 结语 如果想要尽可能的利用Loop Interchange优化,那在书写C/C++代码时,请尽可能保证每个迭代之间对数组或数列的访问stride越小越好;stride越接近1,空间局部性就越高,自然cache命中率也会更高,在性能数据上也可以拿到更理想的收益。另外,由于C/C++的存储方式为Row-major ordering,所以在访问多维数组时,请注意内层循环要为Column才能拿到更小的stride。 ## 参考 [1] https://zhuanlan.zhihu.com/p/455263968 [2] https://valgrind.org/info/tools.html#cachegrind [3] https://blog.csdn.net/gengshenghong/article/details/7225775 [4] https://en.wikipedia.org/wiki/Loop_interchange [5] https://johnysswlab.com/loop-optimizations-how-does-the-compiler-do-it/#footnote_6_1738 [6] https://blog.csdn.net/PCb4jR/article/details/85241114 [7] https://blog.csdn.net/Darlingqiang/article/details/118913291 [8] https://en.wikipedia.org/wiki/Row-_and_column-major_order [9] https://en.wikipedia.org/wiki/Locality_of_reference ---- 欢迎加入Compiler SIG交流群与大家共同交流学习编译技术相关内容,扫码添加小助手微信邀请你进入Compiler SIG交流群。 ![](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/20228/1/1659328114468516528.jpg) 原文转载自 毕昇编译-[编译器工程师眼中的好代码(1):Loop Interchange](https://mp.weixin.qq.com/s/arD0w1NbUU6PNybuxUfVIQ )
  • [技术干货] 图形化开发物联网编解码插件
    体验名称:0代码图形化开发物联网编解码插件内容简介:由于物联网设备资源受限、低功耗、或考虑省流等,一般采用二进制码流的格式上报数据。由此需在云端通过编解码插件将二进制格式转换为符合物模型的Json格式。本此体验可体验通过图形化编解码工具拖拉拽的方式,免开发、0代码快速生成插件,提升开发效率。本实验通过免开发、0代码的产品体验,强化开发者对IoT平台的理解以及物联网开发的相关流程概念;降低开发者使用物联网平台的门槛,让开发者快速理解物联网设备接入的开发过程。本节首先以一个NB-IoT烟感设备的例子讲解如何开发一个支持数据上报和命令下发的编解码插件,并且支持上报命令执行结果,然后再以两个场景举例说明如何完成复杂的插件开发以及调试。前提条件注册华为云官方帐号。未注册可单击注册页面完成注册。完成实名制认证。未完成可在华为云上单击实名认证完成认证,否则无法使用设备接入功能。开通设备接入服务。未开通则访问设备接入服务,单击“立即使用”后开通该服务。数据上报和命令下发场景说明有一款烟感设备,具有如下特征:具有烟雾报警功能(火灾等级)和温度上报功能。支持远程控制命令,可远程打开报警功能。比如火灾现场温度,远程打开烟雾报警,提醒住户疏散。支持上报命令执行结果。产品模型定义登录设备接入服务管理控制台,单击左侧导航栏“产品”,单击页面右上角的“创建产品”。在烟感产品的开发空间,完成产品模型定义。level:火灾级别,用于表示火灾的严重程度。temperature:温度,用于表示火灾现场温度。SET_ALARM:打开或关闭告警命令,value=0表示关闭,value=1表示打开。编解码插件开发在烟感产品的开发空间,选择“插件开发”,单击“图形化开发”。单击“新增消息”,新增“smokerinfo”消息。配置此步骤的主要目的是,将设备上传的二进制码流消息解码成JSON格式,以便物联网平台理解。配置示例如下:消息名:smokerinfo消息类型:数据上报。添加响应字段:是。添加响应字段后,物联网平台在收到设备上报的数据后,会下发用户设置的响应数据到设备。响应数据:AAAA0000(默认)单击“添加字段”,勾选“标记为地址域”,添加messageId字段,表示消息类型。在本场景中,上报火灾等级和温度的消息类型是0x0。设备上报消息时,每条消息首个字段就是messageId。如设备上报消息为0001013A,第一个字段00就是表示此条消息是上报火灾级别和温度的消息。后续字段01和013A分别代表火灾级别和温度。如果只有一条数据上报消息和一条命令下发消息,可以不添加messageId字段。“数据类型”根据数据上报消息种类的数量进行配置。messageId字段默认的数据类型为int8u。“偏移值”是根据字段位置和字段的字节数的配置自动填充的。messageId为此消息的第一个字段,起始位置为0,字节长度为1,终点位置为1。所以偏移值为0-1。“长度”是根据“数据类型”的配置自动填充的。“默认值”可以修改,但必须为十六进制格式,且设备数据上报消息的对应字段必须和此处的默认值保持一致。添加level字段,表示火灾级别。“字段名”只能输入包含字母、数字、_和$,且不能以数字开头的字符。“数据类型”根据设备上报数据的实际情况进行配置,需要和产品模型相应字段的定义相匹配。产品模型中定义的火灾级别level属性的数据类型为int,最大值为9。所以选择的数据类型为int8u。“偏移值”是根据字段位置和字段的字节数的配置自动填充的。“level”字段的起始位置就是前一字段的终点,前一字段“messageId”的终点位置为1,所以“level”字段的起始位置为1。“level”字段长度为1个字节,终点为2。所以“偏移值”为1-2。“长度”根据“数据类型”的配置自动填充。“默认值”不填。此处火灾级别level不固定,无默认值。添加temperature字段,表示温度。“数据类型”,在产品模型中,temperature属性的“数据类型”为int,最大值1000,因此在插件中定义temperature字段的“数据类型”为“int16u”,以满足temperature属性的取值范围。“偏移值”是根据与首字段的间隔的字符数自动配置的。“temperature”字段的起始位置就是前一字段的终点,前一字段“level”的终点位置为2,所以“temperature”字段的起始位置为2。“temperature”字段长度为2个字节,终点为4。所以“偏移值”为2-4。“长度”根据数据类型的配置自动填充。“默认值”不填,此处温度temperature的值不固定,无默认值。单击“新增消息”,新增“SET_ALARM”消息,设置火灾告警的温度阈值。例如超过60摄氏度,设备上报告警。配置此步骤的主要目的是,将平台下发的JSON格式命令消息编码成二进制数据,以便烟感设备理解。配置示例如下:消息名:SET_ALARM消息类型:命令下发添加响应字段:是。添加响应字段后,设备在接收命令后,可以上报命令执行结果。您可以根据自己的需求,选择是否添加响应字段。单击“添加字段”,添加messageId字段,表示消息类型。例如,设置火灾告警阈值的消息类型为0x3。messageId、数据类型、长度、默认值、偏移值的说明可参考1。添加mid字段。这里的mid字段是由平台生成和下发的,用于将下发的命令和命令下发响应消息关联。mid字段的数据类型默认为int16u。长度、默认值、偏移值的说明可参考2。添加value字段,表示下发命令的参数值。例如,下发火灾告警的温度阈值。数据类型、长度、默认值、偏移值的说明可以参考2。单击“添加响应字段”,添加“messageId”字段,表示消息类型。命令下发响应消息为上行消息,需要通过messageId和数据上报消息进行区分。上报火灾告警温度阈值的消息类型为0x4。messageId、数据类型、长度、默认值、偏移值的说明可参考1。添加mid字段。这里的mid字段需要跟平台下发的命令里的mid字段保持一致,用于将下发的命令和命令执行结果进行关联。mid字段的数据类型默认为int16u。长度、默认值、偏移值的说明可参考2。添加errcode字段,用于表示命令执行状态:00表示成功,01表示失败,如果未携带该字段,则默认命令执行成功。errcode字段的数据类型默认为int8u。长度、默认值、偏移值的说明可参考2。添加result字段,用于表示命令执行结果。例如,设备向平台返回当前的告警阈值。拖动右侧“设备模型”区域的属性字段和命令字段,数据上报消息和命令下发消息的相应字段建立映射关系。单击“保存”,并在插件保存成功后单击“部署”,将编解码插件部署到物联网平台。保存成功后,请回到产品-插件开发页面,将页面截图,并将截图粘贴到本帖评论区,作为完成任务的证明。至此本次体验已结束,后续步骤大家可选择性操作。调测编解码插件在烟感产品的开发空间,选择“在线调试”,并单击“新增测试设备”。用户可根据自己的业务场景,选择使用真实设备或者模拟设备进行调测。具体调测步骤请参考在线调试。本文以模拟设备为例,调测编解码插件。在弹出的“新增测试设备”窗口,选择“虚拟设备”,单击“确定”,创建一个虚拟设备。虚拟设备名称包含 “Simulator”字样,每款产品下只能创建一个虚拟设备。单击“调试”,进入调试界面。使用设备模拟器进行数据上报。十六进制码流示例:0008016B。00为地址域meaasgeID,08表示火灾级别level,长度为1个字节;016B表示温度,长度为2个字节。在“应用模拟器”区域查看数据上报的结果:{level=8, temperature=363}。8为十六进制数08转换为十进制的数值;363为十六进制数016B转换为十进制的数值。在设备模拟器区域看到平台下发的响应数据AAAA0000。使用应用模拟器进行命令下发,输入value值为1,可看到应用模拟下发命令{ "serviceId": "Smokeinfo", "method": "SET_ALARM", "paras": "{\"value\":1}" }。在“设备模拟器”区域查看命令接收的结果:03000201。03为地址域messageId,0002为mid字段,01为十进制数1转换为十六进制的数值。总结如果插件需要对命令执行结果进行解析,则必须在命令和命令响应中定义mid字段。命令下发的mid是2个字节,对于每个设备来说,mid从1递增到65535,对应码流为0001到FFFF。设备执行完命令,命令执行结果上报中的mid要与收到命令中的mid保持一致,这样平台才能刷新对应命令的状态。字符串及可变长字符串的编解码插件如果该烟感设备需要支持描述信息上报功能,描述信息支持字符串和可变长度字符串两种类型,则按照以下步骤创建消息。产品模型定义在烟感产品的开发空间完成产品模型定义。编解码插件开发在烟感产品的开发空间,选择“插件开发”,单击“图形化开发”。单击“新增消息”,新增消息“otherinfo”,上报字符串类型的描述信息。配置此步骤的主要目的是,将设备上传的字符串二进制码流消息解码成JSON格式,以便物联网平台理解。配置示例如下:消息名:otherinfo消息类型:数据上报添加响应字段:是。添加响应字段后,物联网平台在收到设备上报的数据后,会下发用户设置的响应数据到设备。响应数据:AAAA0000(默认)单击“添加字段”,添加messageId字段,表示消息种类。在本场景中,0x0用于标识上报火灾等级和温度的消息,0x1用于标识只上报温度的消息,0x2用于标识上报描述信息(字符串类型)的消息。messageId、数据类型、长度、默认值、偏移值的说明可参考1。添加other_info字段,表示字符串类型的描述信息。在本场景中,字符串类型的字段数据类型选择“string”,“长度”配置 6个字节。字段名、默认值、偏移值的说明可参考2。单击“新增消息”,新增“other_info2”消息名,配置数据上报消息,上报可变长度字符串类型的描述信息。配置此步骤的主要目的是,将设备上传的可变长度字符串二进制码流消息解码成JSON格式,以便物联网平台理解。配置示例如下:消息名:other_info2消息类型:数据上报添加响应字段:是。添加响应字段后,物联网平台在收到设备上报的数据后,会下发用户设置的响应数据到设备。响应数据:AAAA0000(默认)添加messageId字段,表示消息种类。在本场景中,0x0用于标识上报火灾等级和温度的消息,0x1用于标识只上报温度的消息,0x3用于标识上报描述信息(可变长度字符串类型)的消息。messageId、数据类型、长度、默认值、偏移值的说明可参考1。添加length字段,表示可变字符串长度。“数据类型”根据可变长度字符串的长度进行配置,此场景可变字符串长度在255以内,配置为“int8u”。长度、默认值、偏移值的说明可参考2。添加other_info字段,数据类型选择“varstring”,表示可变长度字符串类型的描述信息。“长度关联字段”选择“length”,“长度关联字段差值”和“数值长度”自动填充。“掩码”默认为“0xff”。偏移值说明可参考2。拖动右侧“设备模型”区域的属性字段,与数据上报消息的相应字段建立映射关系。单击“保存”,并在插件保存成功后单击“部署”,将编解码插件部署到物联网平台。调测编解码插件在烟感产品的开发空间,选择“在线调试”,并单击“新增测试设备”。用户可根据自己的业务场景,选择使用真实设备或者模拟设备进行调测。具体调测步骤请参考在线调试。本文以模拟设备为例,调测编解码插件。在弹出的“新增测试设备”窗口,选择“虚拟设备”,单击“确定”,创建一个虚拟设备。虚拟设备名称包含 “Simulator”字样,每款产品下只能创建一个虚拟设备。单击“调试”,进入调试界面。使用设备模拟器上报字符串类型的描述信息。十六进制码流示例:0231。02表示messageId,此消息上报字符串类型的描述信息;31表示描述信息,长度为1个字节。在“应用模拟器”区域查看数据上报的结果:{other_info=null}。描述信息不足6个字节,编解码插件无法解析。十六进制码流示例:02313233343536。02表示messageId,此消息上报字符串类型的描述信息;313233343536表示描述信息,长度为6个字节。在“应用模拟器”区域查看数据上报的结果:{other_info=123456}。描述信息长度为6个字节,编解码插件解析成功。十六进制码流示例:023132333435363738。02表示messageId,此消息上报字符串类型的描述信息;3132333435363738表示描述信息,长度为8个字节。在“应用模拟器”区域查看数据上报的结果:{other_info=123456}。描述信息长度超过6个字节,编解码插件截取前6个字节进行解析。十六进制码流示例:02013132333435。02表示messageId,此消息上报字符串类型的描述信息;013132333435表示描述信息,长度为6个字节。在“应用模拟器”区域查看数据上报的结果:{other_info=\u000112345}。01在ASCII码表里表示“标题开始”,无法用具体字符表示,因此编解码插件解析为\u0001。使用设备模拟器上报可变长度字符串类型的描述信息。十六进制码流示例:030141。03表示messageId,此消息上报可变长度字符串类型的描述信息;01表示描述信息长度(1个字节),长度为1个字节;41表示描述信息,长度为1个字节。在“应用模拟器”区域查看数据上报的结果:{other_info=A}。41是A的十六进制ASCII码。十六进制码流示例:03024142。03表示messageId,此消息上报可变长度字符串类型的描述信息;02表示描述信息长度(2个字节),长度为1个字节;4142表示描述信息,长度为2个字节。在“应用模拟器”区域查看数据上报的结果:{other_info=AB}。4142是AB的十六进制ASCII码。十六进制码流示例:030341424344。03表示messageId,此消息上报可变长度字符串类型的描述信息;03表示描述信息长度(3个字节),长度为1个字节;41424344表示描述信息,长度为4个字节。在“应用模拟器”区域查看数据上报的结果:{other_info=ABC}。描述信息长度超过3个字节,编解码插件截取前3个字节进行解析,414243是ABC的十六进制ASCII码。十六进制码流示例:0304414243。03表示messageId,此消息上报可变长度字符串类型的描述信息;04表示字符串长度(4个字节),长度为1个字节;414243表示描述信息,长度为4个字节。在“应用模拟器”区域查看数据上报的结果:{other_info=null}。描述信息长度不足4个字节,编解码插件解析失败。总结当数据类型为字符串或可变长度字符串时,插件是按照ASCII码进行编解码的:上报数据时,将16进制码流解码为对应字符串,比如:21解析为“!”、31解析为“1”、41解析为“A”;下发命令时,将字符串编码对应的16进制码流,比如:“!”编码为21,“1”编码为31,“A”编码为41。当某字段的数据类型为可变长度字符串时,该字段需要关联长度字段,长度字段的数据类型必须为int。针对可变长度字符串,命令下发和数据上报的编解码插件开发方式相同。图形化开发的编解码插件使用ASCII码16进制的标准表对字符串和可变长度字符串进行编解码。解码时(数据上报),如果解析结果无法使用具体字符表示,如:标题开始、正文开始、正文结束等,则使用\u+2字节码流值表示(例如:01解析为\u0001,02解析为\u0002);如果解析结果可以使用具体字符表示,则使用具体字符。数组及可变长数组数据类型如果该烟感设备需要支持描述信息上报功能,描述信息描述信息支持数组和可变长度数组两种类型,则按照以下步骤创建消息。产品模型定义在烟感产品的开发空间完成产品模型定义。编解码插件开发在烟感产品的开发空间,选择“插件开发”,单击“图形化开发”。单击“新增消息”,新增消息“otherinfo”,上报数组类型的描述信息。配置此步骤的主要目的是,将设备上传的数组二进制码流消息解码成JSON格式,以便物联网平台理解。配置示例如下:消息名:otherinfo消息类型:数据上报添加响应字段:是。添加响应字段后,物联网平台在收到设备上报的数据后,会下发用户设置的响应数据到设备。响应数据:AAAA0000(默认)单击“添加字段”,添加messageId字段,表示消息种类。在本场景中,0x0用于标识上报火灾等级和温度的消息,0x1用于标识只上报温度的消息,0x2用于标识上报描述信息(数组类型)的消息。messageId、数据类型、长度、默认值、偏移值的说明可参考1。添加other_info字段,“数据类型”选择“array”,表示数组类型的描述信息。在本场景中,“长度”配置为5个字节。字段名、默认值、偏移值的说明可参考2。单击“新增消息”,新增“other_info2”消息,上报可变长度数组类型的描述信息。配置此步骤的主要目的是,将设备上传的可变长度数组二进制码流消息解码成JSON格式,以便物联网平台理解。配置示例如下:消息名:other_info2消息类型:数据上报添加响应字段:是。添加响应字段后,物联网平台在收到设备上报的数据后,会下发用户设置的响应数据到设备。响应数据:AAAA0000(默认)单击“添加字段”,添加messageId字段,表示消息种类。在本场景中,0x0用于标识上报火灾等级和温度的消息,0x1用于标识只上报温度的消息,0x3用于标识上报描述信息(可变长度数组类型)的消息。messageId、数据类型、长度、默认值、偏移值的说明可参考1。添加length字段,表示数组长度。“数据类型”根据可变长度数组的长度进行配置,长度在255以内,配置为“int8u”。长度、默认值、偏移值的说明可参考2。添加other_info字段,“数据类型”选择“variant”,表示可变长度数组类型的描述信息。“长度关联字段”选择“length”,“长度关联字段差值”和“数值长度”自动填充。“掩码”默认为“0xff”。偏移值说明可参考2。拖动右侧“设备模型”区域的属性字段,与数据上报消息的相应字段建立映射关系。单击“保存”,并在插件保存成功后单击“部署”,将编解码插件部署到物联网平台。调测编解码插件在烟感产品的开发空间,选择“在线调试”,并单击“新增测试设备”。用户可根据自己的业务场景,选择使用真实设备或者模拟设备进行调测。具体调测步骤请参考在线调试。本文以模拟设备为例,调测编解码插件。在弹出的“新增测试设备”窗口,选择“虚拟设备”,单击“确定”,创建一个虚拟设备。虚拟设备名称包含 “Simulator”字样,每款产品下只能创建一个虚拟设备。单击“调试”,进入调试界面。使用设备模拟器上报数组类型的描述信息。十六进制码流示例:0211223344。02表示messageId,此消息上报数组类型的描述信息;11223344表示描述信息,长度为4个字节。在“应用模拟器”区域查看数据上报的结果:{other_info=null}。描述信息不足5个字节,编解码插件无法解析。十六进制码流示例:021122334455。02表示messageId,此消息上报数组类型的描述信息;1122334455表示描述信息,长度为5个字节。在“应用模拟器”区域查看数据上报的结果:{serviceId: smokedetector, data: {"other_info":"ESIzRFU="}}。描述信息长度为5个字节,编解码插件解析成功。十六进制码流示例:02112233445566。02表示messageId,此消息上报数组类型的描述信息;112233445566表示描述信息,长度为6个字节。在“应用模拟器”区域查看数据上报的结果:{serviceId: smokedetector, data: {"other_info":"ESIzRFU="}}。描述信息长度超过5个字节,编解码插件截取前5个字节进行解析。使用设备模拟器上报可变长度数组类型的描述信息。十六进制码流示例:030101。03表示messageId,此消息上报可变长度数组类型的描述信息;01表示描述信息长度(1个字节),长度为1个字节;01表示描述信息,长度为1个字节。在“应用模拟器”区域查看数据上报的结果:{serviceId: smokedetector, data: {"other_info":"AQ=="}}。AQ==是01经过base64编码后的值。十六进制码流示例:03020102。03表示messageId,此消息上报可变长度数组类型的描述信息;02表示描述信息长度(2个字节),长度为1个字节;0102表示描述信息,长度为2个字节。在“应用模拟器”区域查看数据上报的结果:{serviceId: smokedetector, data: {"other_info":"AQI="}}。AQI=是01经过base64编码后的值。十六进制码流示例:03030102。03表示messageId,此消息上报可变长度数组类型的描述信息;03表示描述信息长度(3个字节),长度为1个字节;0102表示描述信息,长度为2个字节。在“应用模拟器”区域查看数据上报的结果:{other_info=null}。描述信息长度不足3个字节,编解码插件解析失败。十六进制码流示例:0303010203。03表示messageId,此消息上报可变长度数组类型的描述信息;03表示描述信息长度(3个字节),长度为1个字节;010203表示描述信息,长度为3个字节。在“应用模拟器”区域查看数据上报的结果:{serviceId: smokedetector, data: {"other_info":"AQID"}}。AQID是010203经过base64编码后的值。十六进制码流示例:030301020304。03表示messageId,此消息上报可变长度数组类型的描述信息;03表示描述信息长度(3个字节),长度为1个字节;01020304表示描述信息,长度为4个字节。在“应用模拟器”区域查看数据上报的结果:{other_info=AQID}。描述信息长度超过3个字节,编解码插件截取前3个字节进行解析,AQID是010203经过base64编码后的值。base64编码方式说明base64编码方式会把3个8位字节(3*8=24)转化为4个6位字节(4*6=24),并在每个6位字节前补两个0,构成4个8位字节的形式。如果要进行编码的码流不足3个字节,则在码流后用0填充,使用0填充的字节经编码输出的字符为“=”。base64可以将16进制码流当做字符或者数值进行编码,两种方式获得的编码结果不同。以16进制码流01为例进行说明:把01当作字符,不足3个字符,补1个0,得到010。通过查询ASCII码表,将字符转换为8位二进制数,即:0转换为00110000、1转换为00110001,因此010可以转换为001100000011000100110000(3*8=24)。再转换为4个6位字节:001100、000011、000100、110000,并在每个6位字节前补两个0,得到:00001100、00000011、00000100、00110000。这4个8位字节对应的10进制数分别为12、3、4、48,通过查询base64编码表,获得M(12)、D(3)、E(4),由于3个字符中,最后一个字符通过补0获得,因此第4个8位字节使用“=”表示。最终,把01当做字符,通过base64编码得到MDE=。把01当作数值(即1),不足3个字符,补两个0,得到100。将数值转换为8位2进制数,即:0转换为00000000、1转换为00000001,因此100可以转换为000000010000000000000000(3*8=24)。在转换为4个6位字节:000000、010000、000000、000000,并在每个6位字节前补两个0,得到:00000000、00010000、00000000、00000000。这4个8位字节对应的10进制数分别为:0、16、0、0,通过查询base64编码表,获得A(0)、Q(16),由于3个数值中,最后两个数值通过补0获得,因此第3、4个8位字节使用“=”表示。最终,把01当作数值,通过base64编码得到AQ==。总结当数据类型为数组或可变长度数组时,插件是按照base64进行编解码的:上报数据时,将16进制码流进行base64编码,比如:01编码为“AQ==”;命令下发时,将字符进行base64解码,比如:“AQ==”解码为01。当某字段的数据类型为可变长度数组时,该字段需要关联长度字段,长度字段的数据类型必须为int。针对可变长度数组,命令下发和数据上报的编解码插件开发方式相同。图形化开发的编解码插件使用base64进行编码时,是将16进制码流当做数值进行编码。
  • [其他] 浅学Python 变量类型
    Python 变量类型变量是存储在内存中的值,这就意味着在创建变量时会在内存中开辟一个空间。基于变量的数据类型,解释器会分配指定内存,并决定什么数据可以被存储在内存中。因此,变量可以指定不同的数据类型,这些变量可以存储整数,小数或字符。Python 中的变量赋值不需要类型声明。每个变量在内存中创建,都包括变量的标识,名称和数据这些信息。每个变量在使用前都必须赋值,变量赋值以后该变量才会被创建。等号 = 用来给变量赋值。等号 = 运算符左边是一个变量名,等号 = 运算符右边是存储在变量中的值。例如:#!/usr/bin/python# -*- coding: UTF-8 -*- counter = 100 # 赋值整型变量miles = 1000.0 # 浮点型name = "John" # 字符串 print counterprint milesprint name多个变量赋值Python允许你同时为多个变量赋值a = b = c = 1标准数据类型Python 定义了一些标准类型,用于存储各种类型的数据。Python有五个标准的数据类型:    Numbers(数字)    String(字符串)    List(列表)    Tuple(元组)    Dictionary(字典)Python 数字数字数据类型用于存储数值。他们是不可改变的数据类型,这意味着改变数字数据类型会分配一个新的对象。当你指定一个值时,Number 对象就会被创建
  • [技术干货] HashMap?ConcurrentHashMap?相信看完这篇没人能难住你!
    前言Map 这样的 Key Value 在软件开发中是非常经典的结构,常用于在内存中存放数据。本篇主要想讨论 ConcurrentHashMap 这样一个并发容器,在正式开始之前我觉得有必要谈谈 HashMap,没有它就不会有后面的 ConcurrentHashMap。HashMap众所周知 HashMap 底层是基于 数组 + 链表 组成的,不过在 jdk1.7 和 1.8 中具体实现稍有不同。Base 1.71.7 中的数据结构图:先来看看 1.7 中的实现。这是 HashMap 中比较核心的几个成员变量;看看分别是什么意思?初始化桶大小,因为底层是数组,所以这是数组默认的大小。桶最大值。默认的负载因子(0.75)table 真正存放数据的数组。Map 存放数量的大小。桶大小,可在初始化时显式指定。负载因子,可在初始化时显式指定。重点解释下负载因子:由于给定的 HashMap 的容量大小是固定的,比如默认初始化: 1    public HashMap() { 2        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); 3    } 4 5    public HashMap(int initialCapacity, float loadFactor) { 6        if (initialCapacity < 0) 7            throw new IllegalArgumentException("Illegal initial capacity: " + 8                                               initialCapacity); 9        if (initialCapacity > MAXIMUM_CAPACITY)10            initialCapacity = MAXIMUM_CAPACITY;11        if (loadFactor <= 0 || Float.isNaN(loadFactor))12            throw new IllegalArgumentException("Illegal load factor: " +13                                               loadFactor);1415        this.loadFactor = loadFactor;16        threshold = initialCapacity;17        init();18    }给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。因此通常建议能提前预估 HashMap 的大小最好,尽量的减少扩容带来的性能损耗。根据代码可以看到其实真正存放数据的是transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;这个数组,那么它又是如何定义的呢?Entry 是 HashMap 中的一个内部类,从他的成员变量很容易看出:key 就是写入时的键。value 自然就是值。开始的时候就提到 HashMap 是由数组和链表组成,所以这个 next 就是用于实现链表结构。hash 存放的是当前 key 的 hashcode。知晓了基本结构,那来看看其中重要的写入、获取函数:put 方法 1    public V put(K key, V value) { 2        if (table == EMPTY_TABLE) { 3            inflateTable(threshold); 4        } 5        if (key == null) 6            return putForNullKey(value); 7        int hash = hash(key); 8        int i = indexFor(hash, table.length); 9        for (Entry<K,V> e = table[i]; e != null; e = e.next) {10            Object k;11            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {12                V oldValue = e.value;13                e.value = value;14                e.recordAccess(this);15                return oldValue;16            }17        }1819        modCount++;20        addEntry(hash, key, value, i);21        return null;22    }判断当前数组是否需要初始化。如果 key 为空,则 put 一个空值进去。根据 key 计算出 hashcode。根据计算出的 hashcode 定位出所在桶。如果桶是一个链表则需要遍历判断里面的 hashcode、key 是否和传入 key 相等,如果相等则进行覆盖,并返回原来的值。如果桶是空的,说明当前位置没有数据存入;新增一个 Entry 对象写入当前位置。 1    void addEntry(int hash, K key, V value, int bucketIndex) { 2        if ((size >= threshold) && (null != table[bucketIndex])) { 3            resize(2 * table.length); 4            hash = (null != key) ? hash(key) : 0; 5            bucketIndex = indexFor(hash, table.length); 6        } 7 8        createEntry(hash, key, value, bucketIndex); 9    }1011    void createEntry(int hash, K key, V value, int bucketIndex) {12        Entry<K,V> e = table[bucketIndex];13        table[bucketIndex] = new Entry<>(hash, key, value, e);14        size++;15    }当调用 addEntry 写入 Entry 时需要判断是否需要扩容。如果需要就进行两倍扩充,并将当前的 key 重新 hash 并定位。而在 createEntry 中会将当前位置的桶传入到新建的桶中,如果当前桶有值就会在位置形成链表。get 方法再来看看 get 函数: 1    public V get(Object key) { 2        if (key == null) 3            return getForNullKey(); 4        Entry<K,V> entry = getEntry(key); 5 6        return null == entry ? null : entry.getValue(); 7    } 8 9    final Entry<K,V> getEntry(Object key) {10        if (size == 0) {11            return null;12        }1314        int hash = (key == null) ? 0 : hash(key);15        for (Entry<K,V> e = table[indexFor(hash, table.length)];16             e != null;17             e = e.next) {18            Object k;19            if (e.hash == hash &&20                ((k = e.key) == key || (key != null && key.equals(k))))21                return e;22        }23        return null;24    }首先也是根据 key 计算出 hashcode,然后定位到具体的桶中。判断该位置是否为链表。不是链表就根据 key、key 的 hashcode 是否相等来返回值。为链表则需要遍历直到 key 及 hashcode 相等时候就返回值。啥都没取到就直接返回 null 。Base 1.8不知道 1.7 的实现大家看出需要优化的点没有?其实一个很明显的地方就是:当 Hash 冲突严重时,在桶上形成的链表会变的越来越长,这样在查询时的效率就会越来越低;时间复杂度为 O(N)。因此 1.8 中重点优化了这个查询效率。1.8 HashMap 结构图:先来看看几个核心的成员变量: 1    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 2 3    /** 4     * The maximum capacity, used if a higher value is implicitly specified 5     * by either of the constructors with arguments. 6     * MUST be a power of two <= 1<<30. 7     */ 8    static final int MAXIMUM_CAPACITY = 1 << 30; 910    /**11     * The load factor used when none specified in constructor.12     */13    static final float DEFAULT_LOAD_FACTOR = 0.75f;1415    static final int TREEIFY_THRESHOLD = 8;1617    transient Node<K,V>[] table;1819    /**20     * Holds cached entrySet(). Note that AbstractMap fields are used21     * for keySet() and values().22     */23    transient Set<Map.Entry<K,V>> entrySet;2425    /**26     * The number of key-value mappings contained in this map.27     */28    transient int size;和 1.7 大体上都差不多,还是有几个重要的区别:TREEIFY_THRESHOLD 用于判断是否需要将链表转换为红黑树的阈值。HashEntry 修改为 Node。Node 的核心组成其实也是和 1.7 中的 HashEntry 一样,存放的都是 key value hashcode next 等数据。再来看看核心方法。put 方法看似要比 1.7 的复杂,我们一步步拆解:判断当前桶是否为空,空的就需要初始化(resize 中会判断是否进行初始化)。根据当前 key 的 hashcode 定位到具体的桶中并判断是否为空,为空表明没有 Hash 冲突就直接在当前位置创建一个新桶即可。如果当前桶有值( Hash 冲突),那么就要比较当前桶中的 key、key 的 hashcode 与写入的 key 是否相等,相等就赋值给 e,在第 8 步的时候会统一进行赋值及返回。如果当前桶为红黑树,那就要按照红黑树的方式写入数据。如果是个链表,就需要将当前的 key、value 封装成一个新节点写入到当前桶的后面(形成链表)。接着判断当前链表的大小是否大于预设的阈值,大于时就要转换为红黑树。如果在遍历过程中找到 key 相同时直接退出遍历。如果 e != null 就相当于存在相同的 key,那就需要将值覆盖。最后判断是否需要进行扩容。get 方法 1    public V get(Object key) { 2        Node<K,V> e; 3        return (e = getNode(hash(key), key)) == null ? null : e.value; 4    } 5 6    final Node<K,V> getNode(int hash, Object key) { 7        Node<K,V>[] tab; Node<K,V> first, e; int n; K k; 8        if ((tab = table) != null && (n = tab.length) > 0 && 9            (first = tab[(n - 1) & hash]) != null) {10            if (first.hash == hash && // always check first node11                ((k = first.key) == key || (key != null && key.equals(k))))12                return first;13            if ((e = first.next) != null) {14                if (first instanceof TreeNode)15                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);16                do {17                    if (e.hash == hash &&18                        ((k = e.key) == key || (key != null && key.equals(k))))19                        return e;20                } while ((e = e.next) != null);21            }22        }23        return null;24    }get 方法看起来就要简单许多了。首先将 key hash 之后取得所定位的桶。如果桶为空则直接返回 null 。否则判断桶的第一个位置(有可能是链表、红黑树)的 key 是否为查询的 key,是就直接返回 value。如果第一个不匹配,则判断它的下一个是红黑树还是链表。红黑树就按照树的查找方式返回值。不然就按照链表的方式遍历匹配返回值。从这两个核心方法(get/put)可以看出 1.8 中对大链表做了优化,修改为红黑树之后查询效率直接提高到了 O(logn)。但是 HashMap 原有的问题也都存在,比如在并发场景下使用时容易出现死循环。1final HashMap<String, String> map = new HashMap<String, String>();2for (int i = 0; i < 1000; i++) {3    new Thread(new Runnable() {4        @Override5        public void run() {6            map.put(UUID.randomUUID().toString(), "");7        }8    }).start();9}但是为什么呢?简单分析下。看过上文的还记得在 HashMap 扩容的时候会调用 resize() 方法,就是这里的并发操作容易在一个桶上形成环形链表;这样当获取一个不存在的 key 时,计算出的 index 正好是环形链表的下标就会出现死循环。遍历方式还有一个值得注意的是 HashMap 的遍历方式,通常有以下几种: 1Iterator<Map.Entry<String, Integer>> entryIterator = map.entrySet().iterator(); 2        while (entryIterator.hasNext()) { 3            Map.Entry<String, Integer> next = entryIterator.next(); 4            System.out.println("key=" + next.getKey() + " value=" + next.getValue()); 5        } 6 7Iterator<String> iterator = map.keySet().iterator(); 8        while (iterator.hasNext()){ 9            String key = iterator.next();10            System.out.println("key=" + key + " value=" + map.get(key));1112        }强烈建议使用第一种 EntrySet 进行遍历。第一种可以把 key value 同时取出,第二种还得需要通过 key 取一次 value,效率较低。简单总结下 HashMap:无论是 1.7 还是 1.8 其实都能看出 JDK 没有对它做任何的同步操作,所以并发会出问题,甚至出现死循环导致系统不可用。因此 JDK 推出了专项专用的 ConcurrentHashMap ,该类位于 java.util.concurrent 包下,专门用于解决并发问题。坚持看到这里的朋友算是已经把 ConcurrentHashMap 的基础已经打牢了,下面正式开始分析。ConcurrentHashMapConcurrentHashMap 同样也分为 1.7 、1.8 版,两者在实现上略有不同。Base 1.7如图所示,是由 Segment 数组、HashEntry 组成,和 HashMap 一样,仍然是数组加链表。它的核心成员变量:1    /**2     * Segment 数组,存放数据时首先需要定位到具体的 Segment 中。3     */4    final Segment<K,V>[] segments;56    transient Set<K> keySet;7    transient Set<Map.Entry<K,V>> entrySet;Segment 是 ConcurrentHashMap 的一个内部类,主要的组成如下: 1    static final class Segment<K,V> extends ReentrantLock implements Serializable { 2 3        private static final long serialVersionUID = 2249069246763182397L; 4 5        // 和 HashMap 中的 HashEntry 作用一样,真正存放数据的桶 6        transient volatile HashEntry<K,V>[] table; 7 8        transient int count; 910        transient int modCount;1112        transient int threshold;1314        final float loadFactor;1516    }和 HashMap 非常类似,唯一的区别就是其中的核心数据如 value ,以及链表都是 volatile 修饰的,保证了获取时的可见性。原理上来说:ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。下面也来看看核心的 put get 方法。put 方法 1    public V put(K key, V value) { 2        Segment<K,V> s; 3        if (value == null) 4            throw new NullPointerException(); 5        int hash = hash(key); 6        int j = (hash >>> segmentShift) & segmentMask; 7        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck 8             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment 9            s = ensureSegment(j);10        return s.put(key, hash, value, false);11    }首先是通过 key 定位到 Segment,之后在对应的 Segment 中进行具体的 put。 1        final V put(K key, int hash, V value, boolean onlyIfAbsent) { 2            HashEntry<K,V> node = tryLock() ? null : 3                scanAndLockForPut(key, hash, value); 4            V oldValue; 5            try { 6                HashEntry<K,V>[] tab = table; 7                int index = (tab.length - 1) & hash; 8                HashEntry<K,V> first = entryAt(tab, index); 9                for (HashEntry<K,V> e = first;;) {10                    if (e != null) {11                        K k;12                        if ((k = e.key) == key ||13                            (e.hash == hash && key.equals(k))) {14                            oldValue = e.value;15                            if (!onlyIfAbsent) {16                                e.value = value;17                                ++modCount;18                            }19                            break;20                        }21                        e = e.next;22                    }23                    else {24                        if (node != null)25                            node.setNext(first);26                        else27                            node = new HashEntry<K,V>(hash, key, value, first);28                        int c = count + 1;29                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)30                            rehash(node);31                        else32                            setEntryAt(tab, index, node);33                        ++modCount;34                        count = c;35                        oldValue = null;36                        break;37                    }38                }39            } finally {40                unlock();41            }42            return oldValue;43        }虽然 HashEntry 中的 value 是用 volatile 关键词修饰的,但是并不能保证并发的原子性,所以 put 操作时仍然需要加锁处理。首先第一步的时候会尝试获取锁,如果获取失败肯定就有其他线程存在竞争,则利用 scanAndLockForPut() 自旋获取锁。尝试自旋获取锁。如果重试的次数达到了 MAX_SCAN_RETRIES 则改为阻塞锁获取,保证能获取成功。再结合图看看 put 的流程。将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry。遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。最后会解除在 1 中所获取当前 Segment 的锁。get 方法 1    public V get(Object key) { 2        Segment<K,V> s; // manually integrate access methods to reduce overhead 3        HashEntry<K,V>[] tab; 4        int h = hash(key); 5        long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE; 6        if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null && 7            (tab = s.table) != null) { 8            for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile 9                     (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);10                 e != null; e = e.next) {11                K k;12                if ((k = e.key) == key || (e.hash == h && key.equals(k)))13                    return e.value;14            }15        }16        return null;17    }get 逻辑比较简单:只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。ConcurrentHashMap 的 get 方法是非常高效的,因为整个过程都不需要加锁。Base 1.81.7 已经解决了并发问题,并且能支持 N 个 Segment 这么多次数的并发,但依然存在 HashMap 在 1.7 版本中的问题。那就是查询遍历链表效率太低。因此 1.8 做了一些数据结构上的调整。看起来是不是和 1.8 HashMap 结构类似?其中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。也将 1.7 中存放数据的 HashEntry 改为 Node,但作用都是相同的。其中的 val next 都用了 volatile 修饰,保证了可见性。put 方法重点来看看 put 函数:根据 key 计算出 hashcode 。判断是否需要进行初始化。f 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。如果都不满足,则利用 synchronized 锁写入数据。如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。get 方法根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。如果是红黑树那就按照树的方式获取值。就不满足那就按照链表的方式遍历获取值。1.8 在 1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率(O(logn)),甚至取消了 ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对 synchronized 优化是很到位的。总结看完了整个 HashMap 和 ConcurrentHashMap 在 1.7 和 1.8 中不同的实现方式相信大家对他们的理解应该会更加到位。其实这块也是面试的重点内容,通常的套路是:谈谈你理解的 HashMap,讲讲其中的 get put 过程。1.8 做了什么优化?是线程安全的嘛?不安全会导致哪些问题?如何解决?有没有线程安全的并发容器?ConcurrentHashMap 是如何实现的? 1.7、1.8 实现有何不同?为什么这么做?原文链接:https://blog.csdn.net/weixin_44460333/article/details/86770169
  • [技术干货] Random的nextInt()用法
    Random的nextInt()用法一、先看这样一个有趣的题目:1、设计一个密码的自动生成器:密码由大写字母/小写字母/数字组成,生成六位随机密码。2、分别以1、2、3作为种子数创建Random对象,生成六位随机密码进行测试。问题答案核心代码如下:import java.util.Random;import java.util.Scanner;public class RandomTest {public static void main(String[] args) {    // 定义一个字符型数组用来存放密码数据随机取值的内容        char[] pardStore = new char[62];        // 把所有的大写字母放进去         char beg = 'A';        for(int i=0;i<=25;i++)        {            pardStore[i]=beg;                beg=(char)(beg+1);//因为相加所以数据类型会自动转换为int型,所以要强制转换为char型不然不匹配            }        // 把所有的小写字母放进去        char beg1 = 'a';        for(int i=26;i<=51;i++)        {            pardStore[i]=beg1;                beg1=(char)(beg1+1);            }        // 把0到9放进去        char beg2 = '0';        for(int i=52;i<=61;i++)        {            pardStore[i]=beg2;                beg2=(char)(beg2+1);            }        // 分别以1、2、3作为种子数 生成6位随机密码        Scanner sc = new Scanner(System.in);        int seed = sc.nextInt();//到时候seed等同于输入的数字1,2,3作为种子数        Random  random = new Random(seed);        for(int i=1 ; i<=6;i++)        {        int n =random.nextInt(62);//随机生成一个整数,这个整数的范围就是[0,62)        System.out.print(pardStore[n]);    }    }}二、nextInt()用法总结和思考1、nextInt()用法:会随机生成一个整数,这个整数的范围就是int类型的范围-2^31 ~ 2^31-1,但是如果在nextInt()括号中加入一个整数a那么,这个随机生成的随机数范围就变成[0,a)。2、在题目给定数组时如何运用nextInt():上面提出的有趣题目是给定我们一个数组并且用来存放密码的组成元素,在这种给定我们已知的密码组成范围以及用数组来包括并且非同一组成的连续数据时,我们可以借鉴上面的问题答案代码for(int i=1 ; i<=6;i++)//六位数密码所以循环输出六次{int n =random.nextInt(62);//随机生成一个整数,这个整数的范围就是[0,62)System.out.print(pardStore[n]);//每次生成的整数n用pardStore[n]来代表一个数组中的数据}————————————————原文链接:https://blog.csdn.net/rothschild666/article/details/86823311
  • [技术干货] InputStream和OutputStream流的使用
    1.InputStream简介      java.io.InputStream是字节输入流的抽象类,表示字节输入流的所有类的顶层父类。用来将文件中的数据读取到java程序中。如果需要使用输入流的时候,必须创建他的子类进行使用。在读取文件的时候我们常常使用InputStream抽象类的子类FileInputStream文件字节输入流来进行文件中的数据读取,将磁盘中的文件转化为流的形式,读取到内存中来进行使用。2.FileInputStream的使用        FileInputStream的构造方法有:FileInputStream​(File file)参数是传递一个File类型的文件; FileInputStream​(String name)参数是传递一个String类型的文件路径。常用的方法有:int read​()是从文件中读取一个字节数的数据,并返回读取到的这个字节, 如果读取结束,则返回-1;int read​(byte[] b)是一次读取一个字节数组,输入流会把读取到的内容放入到这个字节数组中,并返回读取到的个数, 如果读取结束,则返回-1;void close​()是关闭流,如果不关闭流会造成内存溢出等情况,所以在使用结束后,需要手动调用关闭流的方法进行关闭。3.FileInputStream读取文件3.1 创建一个FileInputStream流对象,并指定需要读取的数据源文件,此时如果指定的数据源的文件不存在,则会抛出File not fount excep异常。3.2 调用read()方法进行数据的读取。3.3 把读取到的这个字节赋值给变量i,判断i是否不等于-1,如果不是-1表示没有读完,还在读,本次读取到了内容,就会继续再循环中对读取到的内容进行处理。3.4 调用close​()方法关闭流进行资源的释放。第一种,一个字节一个字节的读取,这种方法不能读取中文,因为一个中文占多个字节,此时一次读取一个字节会把中文拆开读。public class Test { public static void main(String[] args) throws IOException { FileInputStream stream = new FileInputStream("F:\\demo123\\test\\test.txt"); int i = 0; while ((i = stream.read()) != -1) { System.out.print((char) i); } stream.close(); } }第二种,使用字节输入流一次读取一个字节数组,输入流会把读取到的内容放入到这个字节数组中,并返回读取到的个数, 如果读取结束返回-1,这种会比上一种读取方式效率高很多。public class Test1 { public static void main(String[] args) throws IOException { FileInputStream stream = new FileInputStream("F:\\test123\\test\\test.txt"); byte arr[]=new byte[1024]; int len = 0;//返回读取到的个数 //使用循环开始读取 while((len=stream.read(arr))!=-1){ System.out.println(new String(arr,0,len)); } stream.close(); } }4.FileOutputStream输出流进行文件复制    FileOutputStream文件复制的流程是一边读,一边写,每读取一次就把读取到的内容写到文件中。和上面的一次读取一个字节数组的方式一样,也是采用这种方式进行复制的。4.1 创建一个FileInputStream流对象,并指定需要读取的数据源文件,此时如果指定的数据源的文件不存在,则会抛出File not fount excep异常。4.2 创建字节输出流对象用来写入,如果没有会进行自动创建。4.3 创建一个数组用来读取4.4 从文件中用字节数组读取数组,存储到字节数组中,每读取到一个内容,就把读取到的内容写入到目的地文件public class Test2 { public static void main(String[] args) throws IOException { FileInputStream in= new FileInputStream("F:\\test123\\test\\test.txt"); FileOutputStream out = new FileOutputStream("F:\\test123\\test\\test1.txt"); byte arr[] = new byte[1024]; int len = 0; while ((len = in.read(arr)) != -1) { fs.write(arr,0,len); } in.close(); out.close(); } }以上就是InputStream和FileOutputStream的简单使用方法总结。
  • [其他] 《深度学习入门》笔记 - 04
    然后就是Python的介绍。包括常见的数据类型,基本算术运算,比较和布尔运算,如何载入额外的模块和包。 基本数据结构有列表、元组、字典和集合。控制结构,内建函数和自定义函数。 然后介绍numpy库,他可以实现快速的算数运算,特别是矩阵运算,运算内部是通过C语言实现的,所以比较快。他包含两种基本数据类型:`数组(array)`和`矩阵(matrix)`。 然后介绍基于numpy库的pandas库,可以用于数据分析,数据清理和数据准备。他的数据结构主要有两种:`序列(series)`和`数据表(dataframe)`。 画图工具mathplotlib是在Python中最流行的,可以很方便的实现数据的图形化展示。使用前要载入mathplotlib的`pyplot`。 其实这些知识在华为云的AI gallery里面都有相应的入门教程。
  • [交流吐槽] 第五次笔记
    ## 之前一直遇到的问题 - 描述 - MobaXterm无法重新进行主机连接,每次都报下图错误然后就只能从虚拟机获取IP全部重新来一遍,而且每次获取的IP都是不一样的 - ![e0b87f446be1a3071bd6a8d17765762b.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/20227/28/1658944950026384561.png) - 希望达到的效果 - ip 地址不变,RaiDrive 直接重新连接主机 - 解决方法 - 虚拟机设置成下图 - ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/20227/28/1658945154533335057.png) ## 任务管理 - 介绍 - 概念: 1. 从系统的角度看,任务是竞争系统资源的最小运行单元。任务可以使用或等待CPU、使用内存空间等系统资源,并独立于其它任务运行。 2. LiteOS 的任务模块可以给用户提供多个任务,实现了任务之间的切换和通信,帮助用户管理业务程序流程。这样用户可以将更多的精力投入到业务功能的实现中。 3. LiteOS 中的任务是**抢占式调度机制**,高优先级的任务可打断低优先级任务,低优先级任务必须在高优先级任务阻塞或结束后才能得到调度,同时支持时间片轮转调度方式。 4. LiteOS的任务默认有32个优先级(0-31),最高优先级为0,最低优先级为31。 - 任务状态 - 就绪(Ready): 该任务在就绪列表中,只**等待CPU**。 - 运行(Running): 该任务**正在执行**。 - 阻塞(Blocked): 该任务**不在就绪列表中**。包含任务被挂起、任务被延时、任务正在等待信号量、读写队列或者等待读写事件等。 - 退出态(Dead): 该任务**运行结束**,等待系统回收资源。 - 词条补充 - 任务ID: 在任务创建时通过参数返回给用户,作为任务的一个非常重要的标识。 - 任务优先级: 优先级表示任务执行的优先顺序。 - 任务**入口函数**: 每个新任务得到调度后将执行的函数。 - 任务控制块TCB: 每一个任务都含有一个任务控制块(TCB)。TCB包含了任务上下文栈指针(stack pointer)、任务状态、任务优先级、任务ID、任务名、任务栈大小等信息。**TCB可以反映出每个任务运行情况**。 - 任务栈: 每一个任务都拥有一个独立的栈空间,我们称为任务栈。 - 任务上下文: 任务在运行过程中使用到的一些资源,如寄存器等,我们称为任务上下文。LiteOS 在任务挂起的时候会将本任务的任务上下文信息,保存在自己的任务栈里面,以便任务恢复后,从栈空间中恢复挂起时的上下文信息,从而继续执行被挂起时被打断的代码。 - 任务切换: 任务切换包含获取就绪列表中最高优先级任务、切出任务上下文保存、切入任务上下文恢复等动作。 - 任务调度机制 - 任务状态迁移说明: - 就绪态 → 运行态 : 任务创建后进入就绪态,发生任务切换时,就绪列表中最高优先级的任务被执行,从而进入运行态,但此刻该任务依旧在就绪列表中。 - 运行态→阻塞态:任务运行因挂起、读信号量等待等,在就绪列表中被删除进入阻塞。 - 阻塞态→就绪态(阻塞态→运行态)∶阻塞的任务被恢复后(任务恢复、延时时间超时、读信号量超时或读到信号量等),此时被恢复的任务会被加入就绪列表,从而由阻塞态变成就绪态;此时如果被恢复任务的优先级高于正在运行任务的优先级,则会发生任务切换,将该任务由就绪态变成运行态。 - 就绪态→阻塞态:任务也有可能在就绪态时被阻塞(挂起)。 - 运行态→就绪态:有更高优先级任务创建或者恢复后,发生任务切换而进入就绪列表。 - 运行态→退出态:任务运行结束,内核自动将此任务删除,此时由运行态变为退出态。 - 阻塞态→退出态:阻塞的任务调用删除接口,任务状态由阻塞态变为退出态。 - cmsis_os2的API任务接口简介: - |接口名|功能描述| |--|--| |osThreadNew|创建任务| |osThreadTerminate|删除某个任务(一般是对非自任务操作)| |osThreadSuspend|任务挂起| |osThreadResume|任务恢复| - 创建任务: - ![5d44fceba1ee8ce629f7570ed41208ee.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/20227/28/1658945287785237712.png)``` osThreadNew(osThreadFunc_t func,void * argument,const osThreadAttr_t * attr) ``` - 删除某个任务: ``` osThreadTerminate(osThreadld_t thread_id); ``` - 任务挂起: ``` osThreadSuspend(osThreadld_t thread_id) ``` - 任务恢复: ``` osThreadResume (osThreadld_t thread_id) ``` - 实现任务管理 - 实验结果与扩展 ## 软件定时器 - 定时器相关概念 - 软件定时器,是基于系统 Tick 时钟中断且由软件来模拟的定时器,当经过设定的 Tick 时钟计数值后会触发用户定义的回调函数。定时精度与系统Tick时钟的周期有关。硬件定时器受硬件的限制,数量上不足以满足用户的实际需求,因此为了满足用户需求,提供更多的定时器,LiteOS 操作系统提供软件定时器功能。软件定时器扩展了定时器的数量,允许创建更多的定时业务。软件定时器功能上支持: - 静态裁剪:能通过宏关闭软件定时器功能。 - 软件定时器创建。 - 软件定时器启动。 - 软件定时器停止。 - 软件定时器删除。 - 软件定时器剩余Tick数获取。 - 运作机制 - 软件定时器使用了系统的一个队列和一个任务资源,软件定时器的触发遵循队列规则,先进先出。定时时间短的定时器总是比定时时间长的靠近队列头,满足优先被触发的准则。 - 软件定时器以Tick为基本计时单位,当用户创建并启动一个软件定时器时,Huawei LiteOS会根据当前系统Tick时间及用户设置的定时间隔确定该定时器的到期Tick时间,并将该定时器控制结构挂入计时全局链表。当Tick中断到来时,在Tick中断处理函数中扫描软件定时器的计时全局链表,看是否有定时器超时,若有则将超时的定时器记录下来。 Tick中断处理函数结束后,软件定时器任务(优先级为最高)被唤醒,在该任务中调用之前记录下来的定时器的超时回调函数。 - 实现软件定时器创建 - cmsis os2的API软件定时器接口简介 - |接口名|功能描述| |--|--| |osTimerNew|创建定时器| |osTimerStart|启动定时器| |osTimerStop|停止定时器| |osTimerDelete|删除定时器| - 创建定时器 ``` osTimerNew (osTimerFunc_t func, osTimerType_t type, void *argument, const osTimerAttr t *attr); ``` - 启动定时器 ``` osTimerStart (osTimerld_t timer_id, uint32_t ticks); ``` - 停止定时器 ``` osTimerStop (osTimerld_t timer_id); ``` - 删除定时器 ``` osTimerDelete (osTimerld_t timer_id); ``` ## 信号量 - 基本概念 - 信号量(Semaphore)是一种实现任务间通信的机制,实现任务之间同步或临界资源的互斥访问。常用于协助一组相互竞争的任务来访问临界资源。 - 在多任务系统中,各任务之间需要同步或互斥实现临界资源的保护,信号量功能可以为用户提供这方面的支持3、通常一个信号量的计数值用于对应有效的资源数,表示剩下的可被占用的互斥资源数。其值的含义分两种情况 1. 0,表示没有积累下来的Post信号量操作,且有可能有在此信号量上阻塞的任务。 2. 正值,表示有一个或多个Post信号量操作。 - 以同步为目的的信号量和以互斥为目的的信号量在使用有如下不同: 1. 用作互斥时,信号量创建后记数是满的,在需要使用临界资源时,先取信号量,使其变空,这样其他任务需要使用临界资源时就会因为无法取到信号量而阻塞,从而保证了临界资源的安全。 2. 用作同步时,信号量在创建后被置为空,任务1取信号量而阻塞,任务2在某种条件发生后,释放信号量,于是任务1得以进入READY或RUNNING态,从而达到了两个任务间的同步。 - 运作原理 1. 信号量初始化,为配置的N个信号量申请内存(N值可以由用户自行配置,受内存限制),并把所有的信号量初始化成未使用,并加入到未使用链表中供系统使用。 2. 信号量创建,从未使用的信号量链表中获取一个信号量资源,并设定初值。 3. 信号量申请,若其计数器值大于0,则直接减1返回成功。否则任务阻塞,等待其它任务释放该信号量,等待的超时时间可设定。当任务被一个信号量阻塞时,将该任务挂到信号量等待任务队列的队尾。 4. 信号量释放,若没有任务等待该信号量,则直接将计数器加1返回。否则唤醒该信号量等待任务队列上的第一个任务。 5. 信号量删除,将正在使用的信号量置为未使用信号量,并挂回到未使用链表。 - 信号量运作机制 - 信号量允许多个任务在同一时刻访问同一资源,但会限制同一时刻访问此资源的最大任务数目。访问同一资源的任务数达到该资源的最大数量时,会阻塞其他试图获取该资源的任务,直到有任务释放该信号量。 - 公共资源有四个任务数,信号量都分别被线程1、2、3、4获取后,此时此资源就会锁定而不让线程5进入,线程5及后面的线程都进入阻塞模式,当线程1工作完成而释放出信号量,线程5立即获得信号而得到执行。如此往复。 - 运作示意图:![e9c744d344ba01def54a35983abfa7fa.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/20227/28/1658944795484573505.png)
  • [交流吐槽] 14天鸿蒙设备开发实战学习笔记 第三篇
    华为云14天鸿蒙设备开发实战学习笔记第三篇:内核开发一、 任务管理1. 任务管理1) 从系统的角度看,任务是竞争系统资源的最小运行单元。任务可以使用或等待CPU、使用内存空间等系统资源,并独立于其它任务运行。2) LiteOS的任务模块可以给用户提供多个任务,实现了任务之间的切换和通信,帮助用户管理业务程序流程。这样用户可以将更多的精力投入到业务功能的实现中。3) LiteOS中的任务是抢占式调度机制,高优先级的任务可打断低优先级任务,低优先级任务必须在高优先级任务阻塞或结束后才能得到调度,同时支持时间片轮转调度方式。4) LiteOS的任务默认有32个优先级(O一31),最高优先级为0,最低优先级为31。2. 任务状态任务状态通常分为以下四种:1) 就绪(Ready) :该任务在就绪列表中,只等待CPU。5) 运行(Running) :该任务正在执行。6) 阻塞(Blocked):该任务不在就绪列表中。包含任务被挂起、任务被延时、任务正在等待信号量、读写队列或者等待读写事件等。7) 退出态(Dead)∶该任务运行结束,等待系统回收资源。3. 任务基础概念1) 任务ID:在任务创建时通过参数返回给用户,作为任务的一个非常重要的标识。2) 任务优先级:优先级表示任务执行的优先顺序。3) 任务入口函数:每个新任务得到调度后将执行的函数4) 任务控制块TCB:每一个任务都含有一个任务控制块(TCB)。TCB包含了任务上下文栈指针(stack pointer)、任务状态、任务优先级、任务ID、任务名、任务栈大小等信息。TCB可以反映出每个任务运行情况。5) 任务栈:每一个任务都拥有一个独立的栈空间,我们称为任务栈。6) 任务上下文:任务在运行过程中使用到的一些资源,如寄存器等,我们称为任务上下文。LiteOS在任务挂起的时候会将本任务的任务上下文信息,保存在自己的任务栈里面,以便任务恢复后,从栈空间中恢复挂起时的上下文信息,从而继续执行被挂起时被打断的代码。7) 任务切换:任务切换包含获取就绪列表中最高优先级任务、切出任务上下文保存、切入任务上下文恢复等动作。4. 任务的调度机制1) 就绪态→运行态:任务创建后进入就绪态,发生任务切换时,就绪列表中最高优先级的任务被执行,从而进入运行态,但此刻该任务依旧在就绪列表中。2) 运行态→阻塞态:任务运行因挂起、读信号量等待等,在就绪列表中被删除进入阻塞。3) 阻塞态→就绪态(阻塞态→运行态)︰阻塞的任务被恢复后(任务恢复、延时时间超时、读信号量等),此时被恢复的任务会被加入就绪列表,从而由阻塞态变成就绪态;此时如果被恢复任务的优先级高于正在运行任务的优先级,则会发生任务切换,将该任务由就绪态变成运行态。4) 就绪态→阻塞态:任务也有可能在就绪态时被阻塞(挂起)。5) 运行态→就绪态:有更高优先级任务创建或者恢复后,发生任务切换而进入就绪列表。6) 运行态→退出态:任务运行结束,内核自动将此任务删除,此时由运行态变为退出态。7) 阻塞态→退出态:阻塞的任务调用删除接口,任务状态由阻塞态变为退出态。5. cmsis_os2 API1) osThreadNew:创建任务osThreadNew(osThreadFunc_t func,void* argument,const osThreadAttr_t* attr)参数:    func:任务函数             argument:作为启动参数传递给任务函数的指针             attr:任务入口函数的参数列表             返回值:任务ID2) osThreadTerminate:删除某个任务(一般是对非任务操作)3) osThreadSuspend:任务挂起4) osThreadResume:任务恢复6. 任务代码1) 创建任务查看小熊派主板官方例程:/applications/BearPi/BearPi一HM_Nano/sample/A1_kernal_thread通过osThreadNew()函数创建了thread1和thread2两个进程,thread1和thread2启动后会输出打印日志。【编译运行】运行前需注释掉之前LED的启动代码,并取消注释A1_kernal_thread的项目代码        8) 任务调度二、 软件定时器1. 基本概念软件定时器,是基于系统Tick时钟中断且由软件来模拟的定时器,当经过设定的Tick时钟计数值后会触发用户定义的回调函数。定时精度与系统Tick时钟的周期有关。硬件定时器受硬件的限制,数量上不足以满足用户的实际需求,因此为了满足用户需求,提供更多的定时器,LiteOS操作系统提供软件定时器功能。软件定时器扩展了定时器的数量,允许创建更多的定时业务。软件定时器功能上支持:• 静态裁剪:能通过宏关闭软件定时器功能。• 软件定时器创建。• 软件定时器启动。• 软件定时器停止。软件定时器删除。• 软件定时器剩余Tick数获取。2. 运作机制软件定时器使用了系统的一个队列和一个任务资源,软件定时器的触发遵循队列规则,先进先出。定时时间短的定时器总是比定时时间长的靠近队列头,满足优先被触发的准则。软件定时器以Tick为基本计时单位,当用户创建并启动一个软件定时器时,LiteOS会根据当前系统Tick时间及用户设置的定时间隔确定该定时器的到期Tick时间,并将该定时器控制结构挂入计时全局链表。当Tick中断到来时,在Tick中断处理函数中扫描软件定时器的计时全局链表,看是否有定时器超时,若有则将超时的定时器记录下来。Tick中断处理函数结束后,软件定时器任务(优先级为最高)被唤醒,在该任务中调用之前记录下来的定时器的超时回调函数。3. cmsis_os2的API软件定时器接口简介:1) 创建定时器:osTimerNewosTimerId_t osTimerNew(osTimerFunc_t func, osTimerType_t type, void *argument, const osTimerAttr_t *attr)参数:osTimerFunc_t func:定时器的回调函数        osTimerType_t type    :定时器类型,分为单次和周期,即osTimerPeriodic        void *argument:定时器的回调函数参数        const osTimerAttr_t *attr:扩展参数2) 启动定时器: osTimerStartosStatus_t osTimerStart(osTimerId_t timer_id, uint32_t ticks)参数:    osTimerId_t timer_id:创建的定时器ID    uint32_t ticks: 设置定时器时间(Hi3861 1U=10ms,100U=1S)3) 停止定时器: osTimerStoposStatus_t osTimerStop(osTimerId_t timer_id)4) 删除定时器: osTimerDeleteosStatus_t osTimerDelete(osTimerId_t timer_id)三、 信号量1. 基本概念1) 信号量(Semaphore)是一种实现任务间通信的机制,实现任务之间同步或临界资源的互斥访问。常用于协助一组相互竞争的任务来访问临界资源。2) 在多任务系统中,各任务之间需要同步或互斥实现临界资源的保护,信号量功能可以为用户提供这方面的支持。3) 通常一个信号量的计数值用于对应有效的资源数,表示剩下的可被占用的互斥资源数。其值的含义分两种情况:%2) 表示没有积累下来的Post信号量操作,且有可能有在此信号量上阻塞的任务。%2) 正值,表示有一个或多个Post信号量操作。4) 以同步为目的的信号量和以互斥为目的的信号量在使用有如下不同:a) 用作互斥时,信号量创建后记数是满的,在需要使用临界资源时先取信号量,使其变空,这样其他任务需要使用临界资源时就会因为无法取到信号量而阻塞,从而保证了临界资源的安全。b) 用作同步时,信号量在创建后被置为空,任务1取信号量而阻塞,任务2在某种条件发生后,释放信号量,于是任务1得以进入READY或RUNNING态,从而达到了两个任务间的同步。2. 运作机制1) 信号量初始化,为配置的N个信号量申请内存(N值可以由用户自行配置,受内存限制),并把所有的信号量初始化成未使用,并加入到未使用链表中供系统使用。2) 信号量创建,从未使用的信号量链表中获取一个信号量资源,并设定初值。3) 信号量申请,若其计数器值大于O,则直接减1返回成功。否则任务阻塞,等待其它任务释放该信号量,等待的超时时间可设定。当任务被一个信号量阻塞时,将该任务挂到信号量等待任务队列的队尾。4) 信号量释放,若没有任务等待该信号量,则直接将计数器加1返回。否则唤醒该信号量等待任务队列上的第一个任务。5) 信号量删除,将正在使用的信号量置为未使用信号量,并挂回到未使用链表。6) 信号量允许多个任务在同一时刻访问同一资源,但会限制同一时刻访问此资源的最大任务数目。访问同一资源的任务数达到该资源的最大数量时,会阻塞其他试图获取该资源的任务,直到有任务释放该信号量。3. cmsis_os2的API信号量接口简介:1) 创建信号量:osSemaphoreNewosSemaphoreNew(uint32_t max_count, uint32_t initial_count, const osSemaphoreAttr_t *attr)max_count:最大数值initial_count:初始值    attr:扩展参数2) 获取信号量:osSemaphoreAcquireosStatus_t osSemaphoreAcquire(osSemaphoreId_t semaphore_id, uint32_t timeout)semaphore_id:信号量IDtimeout:等待获取的最大时间3) 释放信号量:osSemaphoreReleaseosStatus_t osSemaphoreRelease(osSemaphoreId_t semaphore_id)semaphore_id:信号量ID4) 删除信号量:osSemaphoreDelete    osStatus_t osSemaphoreDelete(osSemaphoreId_t semaphore_id)semaphore_id:信号量ID四、 事件1. 基本概念事件是一种实现任务间通信的机制,可用于实现任务间的同步,但事件通信只能是事件类型的通信,无数据传输。一个任务可以等待多个事件的发生:可以是任意一个事件发生时唤醒任务进行事件处理;也可以是几个事件都发生后才唤醒任务进行事件处理。事件集合用32位无符号整型变量来表示,每一位代表一个事件。多任务环境下,任务之间往往需要同步操作。事件可以提供一对多多对多的同步操作。一对多同步模型: 一个任务等待多个事件的触发;多对多同步模型:多个任务等待多个事件的触发。任务可以通过创建事件控制块来实现对事件的触发和等待操作。LiteOS的事件仅用于任务间的同步。2. 运行机制读事件时,可以根据入参事件掩码类型uwEventMask读取事件的单个或者多个事件类型。事件读取成功后,如果设置LOS_ WAITMODE CLR会清除已读取到的事件类型,反之不会清除已读到的事件类型,需显式清除。可以通过入参选择读取模式,读取事件掩码类型中所有事件还是读取事件掩码类型中任意事件。写事件时,对指定事件写入指定的事件类型,可以一次同时写多个事件类型。写事件会触发任务调度。清除事件时,根据入参事件和待清除的事件类型,对事件对应位进行清0操作。3. cmsis_os2的API信号量接口简介:1) 创建事件标记对象: osEventFlagsNewosEventFlagsId_t osEventFlagsNew(const osEventFlagsAttr_t *attr)attr:扩展参数2) 设置事件标记: osEventFlagsSetuint32_t osEventFlagsSet(osEventFlagsId_t ef_id, uint32_t flags)ef_id:事件IDflags:发送的标志3) 等待事件标记触发: uint32_t osEventFlagsWait(osEventFlagsId_t ef_id, uint32_t flags, uint32_t options, uint32_t timeout)ef_id:事件IDflags:标志options:定义多个事件同时触发或者任一条件触发,如osFlagsWaitAny或osFlagsWaitAlltimeout:4) 删除事件标记对象: osStatus_t osEventFlagsDelete(osEventFlagsId_t ef_id)ef_id:事件ID五、 互斥锁1. 互斥锁的概念:1) 互斥锁又称互斥型信号量,是一种特殊的二值性信号量,用于实现对共享资源的独占式处理。2) 任意时刻互斥锁的状态只有两种:开锁或闭锁。3) 当有任务持有时,互斥锁处于闭锁状态,这个任务获得该互斥锁的所有权。4) 当该任务释放时,该互斥锁被开锁,任务失去该互斥锁的所有权。5) 当一个任务持有互斥锁时,其他任务将不能再对该互斥锁进行开锁或持有。6) 多任务环境下往往存在多个任务竞争同一共享资源的应用场景,互斥锁可被用于对共享资源的保护从而实现独占式访问。另外,互斥锁可以解决信号量存在的优先级翻转问题。LiteOS提供的互斥锁具有如下特点:通过优先级继承算法,解决优先级翻转问题。2. 运作机制多任务环境下会存在多个任务访问同一公共资源的场景,而有些公共资源是非共享的,需要任务进行独占式处理。互斥锁怎样来避免这种冲突呢?用互斥锁处理非共享资源的同步访问时,如果有任务访问该资源,则互斥锁为加锁状态。此时其他任务如果想访问这个公共资源则会被阻塞,直到互斥锁被持有该锁的任务释放后,其他任务才能重新访问该公共资源,此时互斥锁再次上锁,如此确保同一时刻只有一个任务正在访问这个公共资源,保证了公共资源操作的完整性。3. cmsis os2的API互斥锁接口简介:1) 创建互斥锁:osMutexId_t osMutexNew(const osMutexAttr_t *attr)attr:扩展参数2) 获取互斥锁:osStatus_t osMutexAcquire(osMutexId_t mutex_id, uint32_t timeout)mutex_id:互斥锁IDtimeout:超时时间3) 释放互斥锁:osStatus_t osMutexRelease(osMutexId_t mutex_id)mutex_id:互斥锁ID4) 删除互斥锁:osStatus_t osMutexDelete(osMutexId_t mutex_id)mutex_id:互斥锁ID六、 消息队列1. 基本概念消息队列,是一种常用于任务间通信的数据结构,实现了接收来自任务或中断的不固定长度的消息,并根据不同的接口选择传递消息是否存放在自己空间。任务能够从队列里面读取消息,当队列中的消息是空时,挂起读取任务;当队列中有新消息时,挂起的读取任务被唤醒并处理新消息。用户在处理业务时,消息队列提供了异步处理机制,允许将一个消息放入队列,但并不立即处理它,同时队列还能起到缓冲消息作用。LiteOS中使用队列数据结构实现任务异步通信工作,具有如下特性:• 消息以先进先出方式排队, 支持异步读写工作方式。• 读队列和写队列都支持超时机制。• 发送消息类型由通信双方约定,可以允许不同长度(不超过队列节点最大值)消息。• 一个任务能够从任意-个消息队列接收和发送消息。• 多个任务能够从同一个消息队列接收和发送消息。• 当队列使用结束后,如果是动态申请的内存,需要通过释放内存函数回收。2. cmsis_os2的API消息队列接口简介:1) 创建消息队列:osMessageQueueNewosMessageQueueId_t osMessageQueueNew(uint32_t msg_count, uint32_t msg_size, const osMessageQueueAttr_t *attr)msg_count:消息数msg_size:    消息队列中的消息大小attr:扩展参数2) 发送消息:osMessageQueuePutosStatus_t osMessageQueuePut(osMessageQueueId_t mq_id, const void *msg_ptr, uint8_t msg_prio, uint32_t timeout)mq_id:消息IDmsg_ptr:消息指针msg_prio:消息队列timeout:超时时间3) 获取消息:osMessageQueueGetosStatus_t osMessageQueueGet(osMessageQueueId_t mq_id, void *msg_ptr, uint8_t *msg_prio, uint32_t timeout)mq_id:消息IDmsg_ptr:消息指针msg_prio:消息队列timeout:超时时间4) 删除消息队列:osMessageQueueDeleteosStatus_t osMessageQueueDelete(osMessageQueueId_t mq_id)mq_id:消息ID
  • [技术干货] HarmonyOS上实现一个手绘板
    最近在学习 OpenHarmony,恰好之前了解过 canvas,所以本篇文章分享一下我实现的一个手绘板,利用 OpenHarmony 内置的 API cnavas 组件实现。这是一个手绘板,并且可以根据滑动屏幕速度,动态生成线条大小,当用户触摸屏幕,会生成线条,并且速度越快,线条越细。原理分析①绘制原理使用前,需要线了解 canvas 组件,可以参考 HarmonyOS 开发者文档,文档介绍的非常详细,这里就不多介绍了首先,我们需要将 canvas 上下文对象,需要在触摸移动事件中绑定,因为我们是通过触摸来生成对应线条的。然后,属性选择 lineCap,属性值有三种:butt、round、square,我尝试了后发现 round 效果比较好。属性值为 butt 时的效果: 属性值为 round: 属性值为 square: 其实 butt 效果也还行,就是锯齿太严重,虽然 API 中有内置抗锯齿属性,但是不知道为啥设置了没有效果,可能粒度太大了,现在这个粒度已经有点卡了,如果把粒度小设置小一点估计更卡。综上还是选择了 round,它会将线端点以圆形结束,所以效果上更圆润。最后将数组中的最后一个值取出,作为 moveTo 的坐标,将鼠标移动后的点作为 lineTo 的坐标,然后再通过 stroke 就能绘制出图像。绘制直线,通常使用 moveTo () 与 lineTo () 两个方法。moveTo () 方法用于将画笔移至指定点并以改点为直线的开始点,lineTo () 则为结束点。        const el = this.$refs.canvas;        this.ctx = el.getContext('2d')        this.ctx.lineWidth =this.lineWidth/2        this.ctx.beginPath()        // 向线条的每个末端添加圆形线帽。        this.ctx.lineCap = 'square'        // 每次将数组中最后一个值取出,作为起始点        this.ctx.moveTo(this.ArrX[this.ArrX.length-1],this.ArrY[this.ArrY.length-1])        this.ctx.lineTo(e.touches[0].localX,e.touches[0].localY)        this.ctx.stroke()        this.ArrX.push(e.touches[0].localX)        this.ArrY.push(e.touches[0].localY)②线条粗细想要通过速度来计算线条粗细,那么可以是需要获取两点之间的时间,通过时间和距离得到速度。当触发 touchmove 事件,将当前时间戳存储起来,通过上一次触发事件获得的时间;当前触发事件获得的时间,就可以得到两次触发事件的事件间隔,此时我们就获得了两点之间的时间。再计算两点之间的距离(平方和再开根号),通过 路程/时间 = 速度计算出两点之间的速度,从而可以动态生成线条粗细。        // 计算线条粗细        const currTime = Date.now()        if(this.startTime !== 0){            const duration = currTime - this.startTime            // 传入倒数第二个点和最后一个点,和持续时间,会返回加速度            const v = this.speed(this.ArrX[this.ArrX.length-2],this.ArrY[this.ArrY.length-2],this.ArrX[this.ArrX.length-1],this.ArrY[this.ArrY.length-1],duration)            this.lineWidth =   this.lineWidth/v            if(this.lineWidth>25){                this.lineWidth = 25            }            if(this.lineWidth<1){                this.lineWidth = 1            }        }        this.startTime = currTime完整代码index.js:// @ts-nocheckexport default {    data: {        ctx:'',        ArrX:[],        ArrY:[],        //        开始时间        startTime:0,        lineWidth:14    },    // 偏移很多    touchstart(e){        //        开始时间清空        this.startTime = 0        this.ArrX.push(e.touches[0].localX)        this.ArrY.push(e.touches[0].localY)    },    //    计算最后两点的速度    speed(x1,y1,x2,y2,s){        const x = Math.abs(x1-x2)*Math.abs(x1-x2)        const y = Math.abs(y1-y2)*Math.abs(y1-y2)        return Math.sqrt(x+y)/s    },    touchmove(e){        // 计算线条粗细        const currTime = Date.now()        if(this.startTime !== 0){            const duration = currTime - this.startTime            // 传入倒数第二个点和最后一个点,和持续时间,会返回加速度            const v = this.speed(this.ArrX[this.ArrX.length-2],this.ArrY[this.ArrY.length-2],this.ArrX[this.ArrX.length-1],this.ArrY[this.ArrY.length-1],duration)            this.lineWidth =   this.lineWidth/v            if(this.lineWidth>25){                this.lineWidth = 25            }            if(this.lineWidth<1){                this.lineWidth = 1            }        }        this.startTime = currTime        const el = this.$refs.canvas;        this.ctx = el.getContext('2d')        this.ctx.lineWidth =this.lineWidth/2        this.ctx.beginPath()        // 向线条的每个末端添加圆形线帽。        this.ctx.lineCap = 'square'        // 每次将数组中最后一个值取出,作为起始点        this.ctx.moveTo(this.ArrX[this.ArrX.length-1],this.ArrY[this.ArrY.length-1])        this.ctx.lineTo(e.touches[0].localX,e.touches[0].localY)        this.ctx.stroke()        this.ArrX.push(e.touches[0].localX)        this.ArrY.push(e.touches[0].localY)    },    touchend(e){        this.startTime = 0    }}index.hml:<div class="container">    <canvas ref="canvas" class="canvas" @touchstart="touchstart"            @touchmove="touchmove" @touchend="touchend"/></div>index.css:.container{    margin: 50px;}.canvas{    height: 100%;    width: 100%;    background-color: #eee;    border: 1px solid #ffc300;}总结不足点:使用体验不是很好,后续还需要优化,最后,通过自定义组件,加深对 HarmonyOS 的开发,共建鸿蒙生态! 转载于OST开源开发者微信公众号
  • [数据处理] &apos;EyeLoad&apos; has no attribute &apos;parent&apos;
    import os from mindspore.dataset import GeneratorDataset import pandas as pd from mindspore import Tensor from mindspore import dtype as mstype import mindspore.dataset.vision.c_transforms as CV import mindspore.dataset.transforms.c_transforms as C from mindspore.dataset.vision import Inter import cv2 """数据读取类和预处理函数定义""" """数据读取,返回的是一个字典,字典内数据类型为张量""" class EyeLoad(GeneratorDataset): def __init__(self, excel_file, root_dir): #super(EyeLoad, self).__init__() self.landmarks_frame = pd.read_excel(excel_file, engine='openpyxl') self.root_dir = root_dir def __len__(self): return len(self.landmarks_frame) def __getitem__(self, idx): img_name = os.path.join(self.root_dir, self.landmarks_frame.iloc[idx,3]) # with cv2.imread(img_name, "rb") as cr: image = cv2.imread(img_name) label = self.landmarks_frame.iloc[idx,7:].values #values函数将表格型数据结构转换成数组 label = label.astype('float') #if self.transform: #image = self.transform(image) image = Tensor(image, mstype.float32) label = Tensor(label, mstype.float32) sample = {'image':image,'label':label} return sample """数据处理函数""" def process_dataset(image_data, batch_size=32, resize= 299, repeat_size=1, num_parallel_workers=1): """ process_dataset for train or test Args: mnist_ds (str): MnistData path batch_size (int): The number of data records in each group resize (int): Scale image data pixels repeat_size (int): The number of replicated data records num_parallel_workers (int): The number of parallel workers """ import mindspore.dataset.vision.c_transforms as CV import mindspore.dataset.transforms.c_transforms as C from mindspore.dataset.vision import Inter from mindspore import dtype as mstype # define some parameters needed for data enhancement and rough justification resize_height, resize_width = resize, resize rescale = 1.0 / 255.0 shift = 0.0 rescale_nml = 1 / 0.3081 shift_nml = -1 * 0.1307 / 0.3081 # according to the parameters, generate the corresponding data enhancement method resize_op = CV.Resize((resize_height, resize_width), interpolation=Inter.LINEAR) rescale_nml_op = CV.Rescale(rescale_nml, shift_nml) rescale_op = CV.Rescale(rescale, shift) hwc2chw_op = CV.HWC2CHW() type_cast_op = C.TypeCast(mstype.int32) c_trans = [resize_op, rescale_op, rescale_nml_op, hwc2chw_op] # using map to apply operations to a dataset image_data = image_data.map(operations=type_cast_op, input_columns="label", num_parallel_workers=num_parallel_workers) image_data = image_data.map(operations=c_trans, input_columns="image", num_parallel_workers=num_parallel_workers) # process the generated dataset buffer_size = 10000 image_data = image_data.shuffle(buffer_size=buffer_size) image_data = image_data.batch(batch_size, drop_remainder=True) image_data = image_data.repeat(repeat_size) return image_data 定义好数据读取类和数据处理函数后,进行操作import mindspore.dataset.vision.py_transforms as py_vision train_data = EyeLoad(excel_file ='OIA-ODIR/Training Set/Annotation/training annotation (Chinese).xlsx', root_dir = 'OIA-ODIR/Training Set/Images/') test_data = EyeLoad(excel_file ='OIA-ODIR/On-site Test Set/Annotation/on-site test annotation (Chinese).xlsx', root_dir = 'OIA-ODIR/On-site Test Set/Images/')能够成功读取,但是数据预处理就报错train_data_process = process_dataset(image_data = train_data) test_data_process = process_dataset(image_data = test_data)报错如下:
  • [技术干货] 通过assign赋予web拾取元素明确的数据类型
    以证件号码(既有纯数字字符串,也有数字字母混合的字符串)为例,通过js代码获取数据对象executeScript后(方法可参考[executeScript]快速完成网页数据的填充/JS脚本操作网页的方法分享/js/),通过assign赋予明确的数据类型为string,而不是让RPA自动识别数据类型,避免后续运行eval表达式、字符串子串截取时出现数据类型不匹配。
  • [执行问题] 【YoloV5】【_preprocess_true_boxes】数组下标越界
    出现的问题:我认为可能是我更换数据集导致的,yaml文件的参数没填对,但又不知道该怎么填。出错的那段transforms.py是这样的:def _preprocess_true_boxes(true_boxes, anchors, in_shape, num_classes, max_boxes, label_smooth, label_smooth_factor=0.1, iou_threshold=0.213): """ Introduction ------------ preprocessing ground truth box Parameters ---------- true_boxes: ground truth box shape as [boxes, 5], x_min, y_min, x_max, y_max, class_id """ anchors = np.array(anchors) num_layers = anchors.shape[0] // 3 anchor_mask = [[6, 7, 8], [3, 4, 5], [0, 1, 2]] true_boxes = np.array(true_boxes, dtype='float32') input_shape = np.array(in_shape, dtype='int32') boxes_xy = (true_boxes[..., 0:2] + true_boxes[..., 2:4]) // 2. # trans to box center point boxes_wh = true_boxes[..., 2:4] - true_boxes[..., 0:2] # input_shape is [h, w] true_boxes[..., 0:2] = boxes_xy / input_shape[::-1] true_boxes[..., 2:4] = boxes_wh / input_shape[::-1] # true_boxes = [xywh] grid_shapes = [input_shape // 32, input_shape // 16, input_shape // 8] # grid_shape [h, w] y_true = [np.zeros((grid_shapes[l][0], grid_shapes[l][1], len(anchor_mask[l]), 5 + num_classes), dtype='float32') for l in range(num_layers)] # y_true [gridy, gridx] anchors = np.expand_dims(anchors, 0) anchors_max = anchors / 2. anchors_min = -anchors_max valid_mask = boxes_wh[..., 0] > 0 wh = boxes_wh[valid_mask] if wh.size != 0: wh = np.expand_dims(wh, -2) # wh shape[box_num, 1, 2] boxes_max = wh / 2. boxes_min = -boxes_max intersect_min = np.maximum(boxes_min, anchors_min) intersect_max = np.minimum(boxes_max, anchors_max) intersect_wh = np.maximum(intersect_max - intersect_min, 0.) intersect_area = intersect_wh[..., 0] * intersect_wh[..., 1] box_area = wh[..., 0] * wh[..., 1] anchor_area = anchors[..., 0] * anchors[..., 1] iou = intersect_area / (box_area + anchor_area - intersect_area) # topk iou topk = 4 topk_flag = iou.argsort() topk_flag = topk_flag >= topk_flag.shape[1] - topk flag = topk_flag.nonzero() for index in range(len(flag[0])): t = flag[0][index] n = flag[1][index] if iou[t][n] < iou_threshold: continue for l in range(num_layers): if n in anchor_mask[l]: i = np.floor(true_boxes[t, 0] * grid_shapes[l][1]).astype('int32') # grid_y j = np.floor(true_boxes[t, 1] * grid_shapes[l][0]).astype('int32') # grid_x k = anchor_mask[l].index(n) c = true_boxes[t, 4].astype('int32') y_true[l][j, i, k, 0:4] = true_boxes[t, 0:4] y_true[l][j, i, k, 4] = 1. # lable-smooth if label_smooth: sigma = label_smooth_factor / (num_classes - 1) y_true[l][j, i, k, 5:] = sigma y_true[l][j, i, k, 5 + c] = 1 - label_smooth_factor else: print(y_true) print(l,j,i,k,5+c) y_true[l][j, i, k, 5 + c] = 1. # best anchor for gt best_anchor = np.argmax(iou, axis=-1) for t, n in enumerate(best_anchor): for l in range(num_layers): if n in anchor_mask[l]: i = np.floor(true_boxes[t, 0] * grid_shapes[l][1]).astype('int32') # grid_y j = np.floor(true_boxes[t, 1] * grid_shapes[l][0]).astype('int32') # grid_x k = anchor_mask[l].index(n) c = true_boxes[t, 4].astype('int32') y_true[l][j, i, k, 0:4] = true_boxes[t, 0:4] y_true[l][j, i, k, 4] = 1. # lable-smooth if label_smooth: sigma = label_smooth_factor / (num_classes - 1) y_true[l][j, i, k, 5:] = sigma y_true[l][j, i, k, 5 + c] = 1 - label_smooth_factor else: y_true[l][j, i, k, 5 + c] = 1. # pad_gt_boxes for avoiding dynamic shape pad_gt_box0 = np.zeros(shape=[max_boxes, 4], dtype=np.float32) pad_gt_box1 = np.zeros(shape=[max_boxes, 4], dtype=np.float32) pad_gt_box2 = np.zeros(shape=[max_boxes, 4], dtype=np.float32) mask0 = np.reshape(y_true[0][..., 4:5], [-1]) gt_box0 = np.reshape(y_true[0][..., 0:4], [-1, 4]) # gt_box [boxes, [x,y,w,h]] gt_box0 = gt_box0[mask0 == 1] # gt_box0: get all boxes which have object if gt_box0.shape[0] < max_boxes: pad_gt_box0[:gt_box0.shape[0]] = gt_box0 else: pad_gt_box0 = gt_box0[:max_boxes] # gt_box0.shape[0]: total number of boxes in gt_box0 # top N of pad_gt_box0 is real box, and after are pad by zero mask1 = np.reshape(y_true[1][..., 4:5], [-1]) gt_box1 = np.reshape(y_true[1][..., 0:4], [-1, 4]) gt_box1 = gt_box1[mask1 == 1] if gt_box1.shape[0] < max_boxes: pad_gt_box1[:gt_box1.shape[0]] = gt_box1 else: pad_gt_box1 = gt_box1[:max_boxes] mask2 = np.reshape(y_true[2][..., 4:5], [-1]) gt_box2 = np.reshape(y_true[2][..., 0:4], [-1, 4]) gt_box2 = gt_box2[mask2 == 1] if gt_box2.shape[0] < max_boxes: pad_gt_box2[:gt_box2.shape[0]] = gt_box2 else: pad_gt_box2 = gt_box2[:max_boxes] return y_true[0], y_true[1], y_true[2], pad_gt_box0, pad_gt_box1, pad_gt_box2下面是yaml文件:# Builtin Configurations(DO NOT CHANGE THESE CONFIGURATIONS unless you know exactly what you are doing) enable_modelarts: True # Url for modelarts data_url: "" train_url: "" checkpoint_url: "" # Path for local data_path: "/cache/data" output_path: "/cache/train" load_path: "/cache/checkpoint_path" device_target: "Ascend" need_modelarts_dataset_unzip: False modelarts_dataset_unzip_name: "train2017" # ============================================================================== # Train options data_dir: "/cache/data/coco2017" per_batch_size: 32 yolov5_version: "yolov5s" pretrained_backbone: "" resume_yolov5: "" pretrained_checkpoint: "" lr_scheduler: "cosine_annealing" lr: 0.013 lr_epochs: "220,250" lr_gamma: 0.1 eta_min: 0.0 T_max: 300 max_epoch: 90 warmup_epochs: 20 weight_decay: 0.0005 momentum: 0.9 loss_scale: 1024 label_smooth: 0 label_smooth_factor: 0.1 log_interval: 100 ckpt_path: "outputs/" ckpt_interval: -1 is_save_on_master: 1 is_distributed: 0 rank: 0 group_size: 1 need_profiler: 0 training_shape: "" resize_rate: 10 is_modelArts: 0 #yolov5-1/output/V0017/outputs/2022-04-06_time_13_35_16/ckpt_0/0-90_329760.ckpt # Eval options pretrained: #"/cache/train/outputs/2022-04-06_time_13_35_16/ckpt_0/0-90_329760.ckpt" log_path: "outputs/" ann_val_file: "" eval_nms_thresh: 0.6 eval_shape: 640 ignore_threshold: 0.7 test_ignore_threshold: 0.001 multi_label: True multi_label_thresh: 0.1 # Export options device_id: 0 batch_size: 1 testing_shape: 640 ckpt_file: "" file_name: "yolov5" file_format: "MINDIR" dataset_path: "" ann_file: "" # Other default config hue: 0.015 saturation: 1.5 value: 0.4 jitter: 0.3 multi_scale: [[320, 320], [352, 352], [384, 384], [416, 416], [448, 448], [480, 480], [512, 512], [544, 544], [576, 576], [608, 608], [640, 640], [672, 672], [704, 704], [736, 736], [768, 768]] num_classes: 1 max_box: 150 # h->w anchor_scales: [[12, 16], [19, 36], [40, 28], [36, 75], [76, 55], [72, 146], [142, 110], [192, 243], [459, 401]] out_channel: 18 #3 * (num_classes + 5) input_shape: [[3, 32, 64, 128, 256, 512, 1], [3, 48, 96, 192, 384, 768, 2], [3, 64, 128, 256, 512, 1024, 3], [3, 80, 160, 320, 640, 1280, 4]] # test_param test_img_shape: [1920, 1080] labels: ['car'] coco_ids: [ 1 ] result_files: './result_Files' --- # Help description for each configuration # Train options data_dir: "Train dataset directory." per_batch_size: "Batch size for Training." pretrained_backbone: "The ckpt file of CspDarkNet53." resume_yolov5: "The ckpt file of YOLOv5, which used to fine tune." pretrained_checkpoint: "The ckpt file of YOLOv5CspDarkNet53." lr_scheduler: "Learning rate scheduler, options: exponential, cosine_annealing." lr: "Learning rate." lr_epochs: "Epoch of changing of lr changing, split with ','." lr_gamma: "Decrease lr by a factor of exponential lr_scheduler." eta_min: "Eta_min in cosine_annealing scheduler." T_max: "T-max in cosine_annealing scheduler." max_epoch: "Max epoch num to train the model." warmup_epochs: "Warmup epochs." weight_decay: "Weight decay factor." momentum: "Momentum." loss_scale: "Static loss scale." label_smooth: "Whether to use label smooth in CE." label_smooth_factor: "Smooth strength of original one-hot." log_interval: "Logging interval steps." ckpt_path: "Checkpoint save location." ckpt_interval: "Save checkpoint interval." is_save_on_master: "Save ckpt on master or all rank, 1 for master, 0 for all ranks." is_distributed: "Distribute train or not, 1 for yes, 0 for no." rank: "Local rank of distributed." group_size: "World size of device." need_profiler: "Whether use profiler. 0 for no, 1 for yes." training_shape: "Fix training shape." resize_rate: "Resize rate for multi-scale training." ann_file: "path to annotation" each_multiscale: "Apply multi-scale for each scale" labels: "the label of train data" multi_label: "use multi label to nms" multi_label_thresh: "multi label thresh" # Eval options pretrained: "model_path, local pretrained model to load" log_path: "checkpoint save location" ann_val_file: "path to annotation" # Export options device_id: "Device id for export" batch_size: "batch size for export" testing_shape: "shape for test" ckpt_file: "Checkpoint file path for export" file_name: "output file name for export" file_format: "file format for export" result_files: 'path to 310 infer result floder'
  • [技术干货] TS基础(1)- 原始数据类型
    JavaScript 的类型分为两种:原始数据类型(Primitive data types)和对象类型(Object types)。原始数据类型包括:布尔值、数值、字符串、null、undefined 以及 ES6 中的新类型 Symbol 和 ES10 中的新类型 BigInt。布尔值注意,使用构造函数 Boolean 创造的对象不是布尔值:let createdByNewBoolean: boolean = new Boolean(1); //报错事实上 new Boolean() 返回的是一个 Boolean 对象:let createdByNewBoolean: Boolean = new Boolean(1);直接调用 Boolean 也可以返回一个 boolean 类型:let createdByBoolean: boolean = Boolean(1);在 TypeScript 中,boolean 是 JavaScript 中的基本类型,而 Boolean 是 JavaScript 中的构造函数。其他基本类型(除了 null 和 undefined)一样,不再赘述。数值let hexLiteral: number = 0xf00d; // ES6 中的二进制表示法 let binaryLiteral: number = 0b1010; // ES6 中的八进制表示法 let octalLiteral: number = 0o744;编译后:var hexLiteral = 0xf00d; // ES6 中的二进制表示法 var binaryLiteral = 10; // ES6 中的八进制表示法 var octalLiteral = 484;字符串// 模板字符串 let sentence: string = `Hello, my name is ${myName}. I'll be ${myAge + 1} years old next month.`;编译后:// 模板字符串 var sentence = "Hello, my name is " + myName + ". I'll be " + (myAge + 1) + " years old next month.";空值JavaScript 没有空值(Void)的概念,在 TypeScript 中,可以用 void 表示没有任何返回值的函数:方法无返回的话,可以使用声明一个 void 类型的变量没有什么用,因为你只能将它赋值为 undefined 和 null(只在 --strictNullChecks 未指定时)Null 和 Undefined与 void 的区别是,undefined 和 null 是所有类型的子类型。也就是说 undefined 类型的变量,可以赋值给 number 类型的变量。(非严格模式)而 void 类型的变量不能赋值给 number 类型的变量