CUDA编程 - 用向量化访存优化 elementwise 核函数 - 学习记录

Cuda elementwise

  • 一、简介
    • 1.1、ElementWise
    • 1.2、 float4 - 向量化访存
  • 二、实践
    • 2.1、如何使用向量化访存
    • 2.2、Cuda elementwise - Add
    • 2.3、Cuda elementwise - Sigmoid
      • 2.3.1、简单的 Sigmoid 函数
      • 2.3.2、ElementWise Sigmoid+ float4(向量化访存)
    • 2.4、Cuda elementwise - relu
      • 2.3.1、简单的 relu 函数
      • 2.3.2、ElementWise relu + float4(向量化访存)
  • 三、不能使用向量化访存的情况
    • Cuda elementwise - Histogram(直方图)
  • 四、完整代码
    • 4.1、sigmoid.cu
    • 4.2、relu.cu
  • 4.3、histogram.cu

一、简介

1.1、ElementWise

Element-wise操作是最基础,最简单的一种核函数的类型,它的计算特点很符合GPU的工作方式:对于每个元素单独做一个算术操作,然后直接输出。虽然简单,但深度学习领域,有很多算子都是这个类型。常见的有Add,Mul,Concat,各种激活Sigmoid,Relu及它们的变体,归一化里的BatchNorm都属于这个类型。

1.2、 float4 - 向量化访存

所谓向量化访存,就是一次性读 4 个 float,而不是单单 1 个

要点:

  • 小数据规模情况下,可以不考虑向量化访存的优化方式
  • 大规模数据情况下,考虑使用向量化访存,且 最好是缩小grid的维度为原来的1/4,避免影响Occupancy
  • float4 向量化访存只对数据规模大的时候有加速效果,数据规模小的时候没有加速效果

float4 的性能提升主要在于访存指令减少了(同样的数据规模,以前需要4条指令,现在只需1/4的指令),指令cache里就能存下更多指令,提高指令cache的命中率。

判断是否用上了向量化访存,是在 nsight compute 看生成的SASS代码里会有没有 LDG.E.128 Rx, [Rx.64]或 STG.E.128 [R6.64], Rx这些指令的存在。有则向量化成功,没有则向量化失败。

在这里插入图片描述

官方参考链接1
官方参考链接2

二、实践

2.1、如何使用向量化访存

c :

#define FLOAT4(value)  *(float4*)(&(value))

宏解释:

对于一个值,先对他取地址,然后再把这个地址解释成 float4
对于这个 float4的指针,对它再取一个值
这样编译器就可以一次读四个 float

c++ :

#define FLOAT4(value) (reinterpret_cast<float4*>(&(value))[0])

将核函数写成 float4 的形式的时候,首先要先使用宏定义,其次要注意线程数的变化。

线程数变化原因:因为一个线程可以处理 4个float了,所以要减少 四倍的线程。 (具体看下面的例子)

2.2、Cuda elementwise - Add

参考链接

2.3、Cuda elementwise - Sigmoid

2.3.1、简单的 Sigmoid 函数

a: Nx1, b: Nx1, c: Nx1, c = sigmoid(a, b)

//sigmoid<<<CeilDiv(N, block_size), block_size>>>(d_A, d_B, N)
//
__global__ void sigmoid(float* x, float* y, int N) {int idx = blockIdx.x * blockDim.x + threadIdx.x;if (idx < N) y[idx] = 1.0f / (1.0f + expf(-x[idx]));
}

2.3.2、ElementWise Sigmoid+ float4(向量化访存)

Sigmoid x: N, y: N y=1/(1+exp(-x)) float4

__global__ void sigmoid_vec4(float4* x, float4* y, int N) {int idx = (blockIdx.x * blockDim.x + threadIdx.x) * 4;if (idx < N) {float4 tmp_x = FLOAT4(x[idx]);float4 tmp_y;tmp_y.x = 1.0f / (1.0f + expf(-tmp_x.x));tmp_y.y = 1.0f / (1.0f + expf(-tmp_x.y));tmp_y.z = 1.0f / (1.0f + expf(-tmp_x.z));tmp_y.w = 1.0f / (1.0f + expf(-tmp_x.w));FLOAT4(y[idx]) = tmp_y;}
}

2.4、Cuda elementwise - relu

2.3.1、简单的 relu 函数

Relu x: N, y: N y=max(0,x)

__global__ void relu(float* x, float* y, int N) {int idx = blockIdx.x * blockDim.x + threadIdx.x;if (idx < N) y[idx] = fmaxf(0.0f, x[idx]);
}

2.3.2、ElementWise relu + float4(向量化访存)

Relu x: N, y: N y=max(0,x) float4

__global__ void relu_float4(float* x, float* y, int N) {int idx = (blockIdx.x * blockDim.x + threadIdx.x) * 4;if (idx < N) {float4 tmp_x = FLOAT4(x[idx]);float4 tmp_y;tmp_y.x = fmaxf(0.0f, tmp_x.x);tmp_y.y = fmaxf(0.0f, tmp_x.y);tmp_y.z = fmaxf(0.0f, tmp_x.z);tmp_y.w = fmaxf(0.0f, tmp_x.w);FLOAT4(y[idx]) = tmp_y;}
}

三、不能使用向量化访存的情况

Cuda elementwise - Histogram(直方图)

x[i] = i-- -- - -- - - -
- - - - -

goal: 统计每个元素出现的次数

①、简单的 Histogram函数

x: Nx1, y: count histogram

__global__ void histogram(int* x, int* y, int N) {int idx = blockIdx.x * blockDim.x + threadIdx.x;if (idx < N) atomicAdd(&(y[x[idx]]), 1);
}

其中 int atomicAdd(int* address, int val);

1、atomicAdd 是 CUDA 中的一个原子加函数,用于实现在多个线程同时修改同一个全局变量的情况下,保证数据一致性和正确性
2、该函数会将原始值和 val 相加,并将结果存储在 address 所指向的内存位置,同时返回原始值
3、当多个线程同时调用 atomicAdd 函数时,CUDA 会自动为它们创建一个硬件级的同步访问机制,从而避免了数据竞争和数据不一致性的问题。

参考链接

②、ElementWise Histogram + float4(向量化访存)

__global__ void histogram_float4(int* x, int* y, int N) {int idx = 4 * (blockIdx.x * blockDim.x + threadIdx.x);if (idx < N) {int4 tmp_y = INT4(x[idx]);atomicAdd(&(y[tmp_y.x]), 1);atomicAdd(&(y[tmp_y.y]), 1);atomicAdd(&(y[tmp_y.z]), 1);atomicAdd(&(y[tmp_y.w]), 1);}
}

运行 histogram_float4 后发现比 histogram 还慢。为什么呢?

1、nvidia 官方回答: Can float4 be used for atomicAdd efficiently?
2、histogram的写入(load)没有向量化指令,只有读取(load)向量化。
3、使用 nsight compute 看生成的SASS,只有 LDG ,没有STG
在这里插入图片描述
4、在一个warp里相邻线程对全局内存y的访问没有合并( 因为在一个warp里面不同线程对全局内存是跳着访问的 )

四、完整代码

4.1、sigmoid.cu

#include <stdio.h>
#include <stdlib.h>
#include <float.h>
#include <vector>
#include<assert.h>
#include <algorithm>
#include <cublas_v2.h>
#include <cuda_runtime.h>#define FLOAT4(value)  *(float4*)(&(value))#define checkCudaErrors(func)               \
{                                   \cudaError_t e = (func);         \if(e != cudaSuccess)                                        \printf ("%s %d CUDA: %s\n", __FILE__,  __LINE__, cudaGetErrorString(e));        \
}__global__ void relu(float* x, float* y, int N){int idx = blockIdx.x * blockDim.x + threadIdx.x;if(idx < N) y[idx] = fmaxf(0.0f, x[idx]);
}__global__ void relu_float4(float* x, float* y, int N){int idx = (blockIdx.x * blockDim.x + threadIdx.x) * 4;if(idx < N){float4 tmp_x = FLOAT4(x[idx]);float4 tmp_y;tmp_y.x = fmaxf(0.0f, tmp_x.x);tmp_y.y = fmaxf(0.0f, tmp_x.y);tmp_y.z = fmaxf(0.0f, tmp_x.z);tmp_y.w = fmaxf(0.0f, tmp_x.w);FLOAT4(y[idx]) = tmp_y;}
}//nvcc -o sigmoid sigmoid.cu && ./sigmoid
//sigmoid<<<CeilDiv(N, block_size), block_size>>>(d_A, d_B, N)
//a: Nx1, b: Nx1, c: Nx1, c = sigmoid(a, b)
__global__ void sigmoid(float* a, float* b, int N) {int idx = blockIdx.x * blockDim.x + threadIdx.x;if (idx < N) b[idx] = 1.0f / (1.0f + expf(-a[idx]));
}__global__ void sigmoid_float4(float* a, float* b, int N)
{int idx = (blockDim.x * blockIdx.x + threadIdx.x) * 4;if(idx < N){float4 tmp_a = FLOAT4(a[idx]);float4 tmp_b;tmp_b.x = 1.0f /(1.0f + expf(-tmp_a.x));tmp_b.y = 1.0f /(1.0f + expf(-tmp_a.y));tmp_b.z = 1.0f /(1.0f + expf(-tmp_a.z));tmp_b.w = 1.0f /(1.0f + expf(-tmp_a.w));FLOAT4(b[idx]) = tmp_b;}
}template <typename T> 
inline T CeilDiv(const T& a, const T& b) {return (a + b - 1) / b;
}int main(){size_t block_size = 128;size_t N = 1 * 1024;size_t bytes_A = sizeof(float) * N;size_t bytes_B = sizeof(float) * N;float* h_A = (float*)malloc(bytes_A);float* h_B = (float*)malloc(bytes_B);for( int i = 0; i < N; i++ ){h_A[i] = (i / 666) * ((i % 2 == 0) ? 1: -1);}float* d_A;float* d_B;checkCudaErrors(cudaMalloc(&d_A, bytes_A));checkCudaErrors(cudaMalloc(&d_B, bytes_B));checkCudaErrors(cudaMemcpy( d_A, h_A, bytes_A, cudaMemcpyHostToDevice));cudaEvent_t start, stop;checkCudaErrors(cudaEventCreate(&start));checkCudaErrors(cudaEventCreate(&stop));float msec = 0;int iteration = 1000;checkCudaErrors(cudaEventRecord(start));for(int i = 0; i < iteration; i++){//sigmoid<<<CeilDiv(N, block_size), block_size>>>(d_A, d_B, N);//sigmoid_float4<<<CeilDiv(N, block_size), block_size/4>>>(d_A, d_B, N);sigmoid_float4<<<CeilDiv(N/4,block_size), block_size>>>(d_A, d_B, N);}checkCudaErrors(cudaEventRecord(stop));checkCudaErrors(cudaEventSynchronize(stop));checkCudaErrors(cudaEventElapsedTime(&msec, start, stop));printf("sigmoid takes %.3f msec\n", msec/iteration);checkCudaErrors(cudaMemcpy(h_B, d_B, bytes_B, cudaMemcpyDeviceToHost));for(int i = 0; i < N; i++){double err = fabs(h_B[i] - 1.0f/(1.0f + expf(-h_A[i])));if(err > 1.e-6) {printf("wrong answer!\n");break;}}cudaFree(d_A);cudaFree(d_B);free(h_A);free(h_B);return 0;
}

编译和运行代码:

nvcc -o sigmoid sigmoid.cu
./sigmoid 

4.2、relu.cu

#include <stdio.h>
#include <stdlib.h>
#include <float.h>
#include <vector>
#include<assert.h>
#include <algorithm>
#include <cublas_v2.h>
#include <cuda_runtime.h>#define FLOAT4(value)  *(float4*)(&(value))#define checkCudaErrors(func)               \
{                                   \cudaError_t e = (func);         \if(e != cudaSuccess)                                        \printf ("%s %d CUDA: %s\n", __FILE__,  __LINE__, cudaGetErrorString(e));        \
}//nvcc -o relu relu.cu && ./relu
//sigmoid<<<CeilDiv(N, block_size), block_size>>>(d_A, d_B, N)
//a: Nx1, b: Nx1, c: Nx1, y = relu(x)
__global__ void relu(float* x, float* y, int N){int idx = blockIdx.x * blockDim.x + threadIdx.x;if(idx < N) y[idx] = fmaxf(0.0f, x[idx]);
}__global__ void relu_float4(float* x, float* y, int N){int idx = (blockIdx.x * blockDim.x + threadIdx.x) * 4;if(idx < N){float4 tmp_x = FLOAT4(x[idx]);float4 tmp_y;tmp_y.x = fmaxf(0.0f, tmp_x.x);tmp_y.y = fmaxf(0.0f, tmp_x.y);tmp_y.z = fmaxf(0.0f, tmp_x.z);tmp_y.w = fmaxf(0.0f, tmp_x.w);FLOAT4(y[idx]) = tmp_y;}
}template <typename T> 
inline T CeilDiv(const T& a, const T& b) {return (a + b - 1) / b;
}int main(){size_t block_size = 128;size_t N = 1 * 1024;size_t bytes_A = sizeof(float) * N;size_t bytes_B = sizeof(float) * N;float* h_A = (float*)malloc(bytes_A);float* h_B = (float*)malloc(bytes_B);for( int i = 0; i < N; i++ ){h_A[i] = (i / 666) * ((i % 2 == 0) ? 1: -1);}float* d_A;float* d_B;checkCudaErrors(cudaMalloc(&d_A, bytes_A));checkCudaErrors(cudaMalloc(&d_B, bytes_B));checkCudaErrors(cudaMemcpy( d_A, h_A, bytes_A, cudaMemcpyHostToDevice));cudaEvent_t start, stop;checkCudaErrors(cudaEventCreate(&start));checkCudaErrors(cudaEventCreate(&stop));float msec = 0;int iteration = 1000;checkCudaErrors(cudaEventRecord(start));for(int i = 0; i < iteration; i++){//relu<<<CeilDiv(N, block_size), block_size>>>(d_A, d_B, N);//relu_float4<<<CeilDiv(N, block_size), block_size/4>>>(d_A, d_B, N);relu_float4<<<CeilDiv(N/4, block_size), block_size>>>(d_A, d_B, N);}checkCudaErrors(cudaEventRecord(stop));checkCudaErrors(cudaEventSynchronize(stop));checkCudaErrors(cudaEventElapsedTime(&msec, start, stop));printf("relu takes %.3f msec\n", msec/iteration);checkCudaErrors(cudaMemcpy(h_B, d_B, bytes_B, cudaMemcpyDeviceToHost));for(int i = 0; i < N; i++){double err = fabs(h_B[i] - fmaxf(0.0f, h_A[i]));if(err > 1.e-6) {printf("wrong answer!\n");break;}}cudaFree(d_A);cudaFree(d_B);free(h_A);free(h_B);return 0;
}

编译和运行代码:

nvcc -o relu relu.cu
./relu

4.3、histogram.cu

#include <stdio.h>
#include <stdlib.h>
#include <float.h>
#include <vector>
#include<assert.h>
#include <algorithm>
#include <cublas_v2.h>
#include <cuda_runtime.h>#define INT4(value) *(int4*)(&(value))
#define FLOAT4(value)  *(float4*)(&(value))#define checkCudaErrors(func)               \
{                                   \cudaError_t e = (func);         \if(e != cudaSuccess)                                        \printf ("%s %d CUDA: %s\n", __FILE__,  __LINE__, cudaGetErrorString(e));        \
}/*
x[i] = i-- -- - -- - - -
- - - - -
考虑一个warp里相邻线程对全局内存y的访问是否合并(coalesced global access)
warp thread[0]: 0, 1, 2, 3, 
warp thread[1]: 4, 5, 6, 7
*/
__global__ void histogram(int* x, int* y, int N){int idx = blockIdx.x * blockDim.x + threadIdx.x;if(idx < N) atomicAdd(&y[x[idx]], 1);
}__global__ void histogram_int4(int* x, int* y, int N) {int idx = 4 * (blockIdx.x * blockDim.x + threadIdx.x);if (idx < N) {int4 tmp_y = INT4(x[idx]);atomicAdd(&(y[tmp_y.x]), 1);atomicAdd(&(y[tmp_y.y]), 1);atomicAdd(&(y[tmp_y.z]), 1);atomicAdd(&(y[tmp_y.w]), 1);}
}template <typename T> 
inline T CeilDiv(const T& a, const T& b) {return (a + b - 1) / b;
}int main(){size_t block_size = 128;size_t N = 32 * 1024 * 1024;size_t bytes_A = sizeof(int) * N;size_t bytes_B = sizeof(int) * N;int* h_A = (int*)malloc(bytes_A);int* h_B = (int*)malloc(bytes_B);for( int i = 0; i < N; i++ ){h_A[i] = i;}int* d_A;int* d_B;checkCudaErrors(cudaMalloc(&d_A, bytes_A));checkCudaErrors(cudaMalloc(&d_B, bytes_B));checkCudaErrors(cudaMemcpy( d_A, h_A, bytes_A, cudaMemcpyHostToDevice));cudaEvent_t start, stop;checkCudaErrors(cudaEventCreate(&start));checkCudaErrors(cudaEventCreate(&stop));float msec = 0;int iteration = 1;checkCudaErrors(cudaEventRecord(start));for(int i = 0; i < iteration; i++){//histogram<<<CeilDiv(N, block_size), block_size>>>(d_A, d_B, N);//histogram_int4<<<CeilDiv(N, block_size), block_size/4>>>(d_A, d_B, N);histogram_int4<<<CeilDiv(N/4, block_size), block_size>>>(d_A, d_B, N);}checkCudaErrors(cudaEventRecord(stop));checkCudaErrors(cudaEventSynchronize(stop));checkCudaErrors(cudaEventElapsedTime(&msec, start, stop));printf("histogram takes %.3f msec\n", msec/iteration);checkCudaErrors(cudaMemcpy(h_B, d_B, bytes_B, cudaMemcpyDeviceToHost));for(int i = 0; i < N; i++){//all ones;double err = fabs(h_B[i] - iteration * 1.0f);if(err > 1.e-6) {printf("wrong answer!\n");break;}}cudaFree(d_A);cudaFree(d_B);free(h_A);free(h_B);return 0;
}

编译和运行代码:

nvcc -o histogram histogram.cu
./histogram

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

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

相关文章

商家入驻平台怎么让资金自动分配给商家

最近很多上线了多商户电商系统的朋友咨询&#xff0c;我们平台的用户支付后&#xff0c;钱进入了我们的对公账户&#xff0c;怎样让钱在走完流程后&#xff0c;自动进入商家的账户呢&#xff1f;今天商淘云为您分享商户入驻平台自动分配给商家资金的三种方法。 首先是平台应建立…

Docker基础(一)

文章目录 1. 基础概念2. 安装docker3. docker常用命令3.1 帮助命令3.2 镜像命令3.3 容器命令3.4 其他命令 4. 使用案例 1. 基础概念 镜像&#xff08;Image&#xff09;&#xff1a;Docker 镜像&#xff08;Image&#xff09;&#xff0c;就相当于是一个 root 文件系统。比如官…

node14下运行项目报错:regeneratorRuntime is not defined

regeneratorRuntime is not defined&#xff0c;这是由于配置babel出错问题&#xff0c;由于使用了es7语法如async/await而当前babel版本过低 解决&#xff1a; npm install -D babel-plugin-transform-runtime babel-runtime 安装完成后在.babelrc文件下配置&#xff1a; &qu…

远程连接Redis

以连接阿里云上的Redis为例 1. 在阿里云安全组中开放端口 2.修改Redis启动时所用的配置文件&#xff08;redis.conf&#xff09; 2.1 修改ip地址 如图&#xff1a;将默认的本地ip bind 127.0.0.1地址改为bind 0.0.0.0 2.2 将保护模式关闭 将默认的 supervised yes 改为 n…

Transformer视频理解学习的笔记

今天复习了Transformer,ViT, 学了SwinTransformer, 还有观看了B站视频理解沐神系列串讲视频上&#xff08;24.2.26未看完,明天接着看&#xff09; 这里面更多论文见&#xff1a;https://github.com/mli/paper-reading/ B站视频理解沐神系列串讲视频下&#xff08;明天接着看&a…

认证模式~

认证方式 基于Cookie和Session的认证方式 基于Cookie和Session的认证是传统的Web应用认证机制。它依赖于HTTP协议无状态的特性&#xff0c;在客户端&#xff08;浏览器&#xff09;和服务器之间保持用户的状态。 工作原理 用户登录&#xff1a;用户通过输入用户名和密码来登…

嵌入式C语言(三)

typeof() 使用typeof可以获取一个变量或表达式的类型。 typeof的参数有两种形式&#xff1a;表达式或类型。 int i;typeof(i) j 20; --> int j 20;typeof(int *) a; -->int *a; int f(); -->typeof(f()) k;--? int k我们可以看出通过typeof获取一个变量的…

合并spark structured streaming处理流式数据产生的小文件

备注&#xff1a; By 远方时光原创&#xff0c;可转载&#xff0c;不能复制到其他平台 背景&#xff1a;做流批一体&#xff0c;湖仓一体的大数据架构&#xff0c;常见的做法就是 数据源->spark Streaming->ODS&#xff08;数据湖&#xff09;->spark streaming->…

Vue 实现页面导出A4标准大小的PDF文件,以及处理图片跨域不能正常展示的问题等

效果预览&#xff1a; 代码流程&#xff1a;首先在utils文件夹下创建htmlToPdf的js工具文件&#xff0c;然后在main.js中注册引用 htmlToPdf.js // 导出页面为PDF格式 import html2Canvas from html2canvas import JsPDF from jspdfexport default {install(Vue, options) {V…

hcia datacom课程学习(1):通信基础

1.总体框架 上图为发送方通过互联网传递信息给接收方的过程。 家用路由器会直接集成上图中的四层&#xff08;vlan&#xff0c;DHCP&#xff0c;静态路由&#xff0c;NAT&#xff0c;PPPoE&#xff09;。 2.网络性能指标 &#xff08;1&#xff09;带宽 单位时间内传输的数…

解析Hadoop三大核心组件:HDFS、MapReduce和YARN

目录 HadoopHadoop的优势 Hadoop的组成HDFS架构设计Yarn架构设计MapReduce架构设计 总结 在大数据时代&#xff0c;Hadoop作为一种开源的分布式计算框架&#xff0c;已经成为处理大规模数据的首选工具。它采用了分布式存储和计算的方式&#xff0c;能够高效地处理海量数据。Had…

pod调度策略 标签管理 资源配额与限额 全局资源配额与限额策略,

打分也是基于可调度节点进行打分资源情况. 指定多个节点,会进行覆盖其之前节点名称 --- kind: Pod apiVersion: v1 metadata:name: myhttp spec:nodeName: node-0001 # 基于节点名称进行调度containers:- name: apacheimage: myos:httpd 基于节点名称的调度策略 标签与调…

数据可视化--了解数据可视化和Excel数据可视化

目录 1.1科学可视化&#xff1a; 可视化是模式、关系、异常 1.2三基色原理&#xff1a; 三基色:红色、绿色和蓝色 1.3Excel数据可视化 1.3.1 excel数据分析-13个图表可视化技巧 1.3.2 excel数据分析-28个常用可视化图表&#xff08;video&#xff09; 1.3.3Excel可视化…

康复训练day2——2024牛客寒假集训营6

一道很好的构造题&#xff0c;受益匪浅。 链接&#xff1a;F-命运的抉择_2024牛客寒假算法基础集训营6 (nowcoder.com)​​​​​​ 题意&#xff1a; 题解 &#xff08;并查集 思维&#xff09;&#xff1a; 首先将存在1的情况特判掉&#xff0c;我们的数组的元素都是> 2的…

2024-02-26(Spark,kafka)

1.Spark SQL是Spark的一个模块&#xff0c;用于处理海量结构化数据 限定&#xff1a;结构化数据处理 RDD的数据开发中&#xff0c;结构化&#xff0c;非结构化&#xff0c;半结构化数据都能处理。 2.为什么要学习SparkSQL SparkSQL是非常成熟的海量结构化数据处理框架。 学…

在having、select子句中使用子查询

目录 在having子句中使用子查询 统计出部门平均工资高于公司平均工资的部门编号、平均工资、部门人数 在select子句中使用子查询 查询每个员工的编号、姓名、职位、部门名称 Oracle从入门到总裁:https://blog.csdn.net/weixin_67859959/article/details/135209645 在havin…

销售线索获取 如何查找更多的销售线索平台

在进行销售工作时&#xff0c;寻找潜在客户和销售线索是非常重要的。只有及时地发现客户的需求和问题&#xff0c;才能更好地进行销售和提供服务。然而&#xff0c;在如今的市场环境中&#xff0c;客户的信息被广泛地分散在各个渠道和媒介上&#xff0c;如果仅靠人工搜索和整理…

如何优化Node.js应用的性能

随着Node.js在Web开发领域的广泛应用&#xff0c;越来越多的开发者开始关注如何优化Node.js应用的性能。优化Node.js应用的性能可以提升应用的响应速度&#xff0c;降低资源消耗&#xff0c;提升用户体验。在本文中&#xff0c;我们将探讨一些优化Node.js应用性能的方法和技巧。…

Nginx重写功能和反向代理

目录 一、重写功能rewrite 1. ngx_http_rewrite_module模块指令 1.1 if 指令 1.2 return 指令 1.3 set 指令 1.4 break 指令 2. rewrite 指令 3. 防盗链 3.1 实现盗链 3.2 实现防盗链 4. 实用网址 二、反向代理 1. 概述 2. 相关概念 3. 反向代理模块 4. 参数配置…

亿道丨三防平板丨如何从多方面选择合适的三防加固平板?

在如今这个信息爆炸的时代&#xff0c;移动设备已经成为我们生活和工作的必备工具。然而&#xff0c;在一些特殊的场合中&#xff0c;普通的平板电脑可能无法满足需求&#xff0c;比如工厂车间、野外作业、极端天气等环境下。此时&#xff0c;三防平板就成了不二之选。那么&…