高效 CUDA 调试:将 NVIDIA Compute Sanitizer 与 NVIDIA 工具扩展结合使用并创建自定义工具

高效 CUDA 调试:将 NVIDIA Compute Sanitizer 与 NVIDIA 工具扩展结合使用并创建自定义工具

NVIDIA Compute Sanitizer 是一款功能强大的工具,可以节省您的时间和精力,同时提高 CUDA 应用程序的可靠性和性能。 在 CUDA 环境中调试代码既具有挑战性又耗时,尤其是在处理数千个线程时。 计算消毒剂可以提供帮助!

在本系列的第一篇文章《高效 CUDA 调试:如何使用 NVIDIA Compute Sanitizer 寻找错误》中,我们讨论了如何开始使用一些 Compute Sanitizer 工具来在调试代码时检查内存泄漏和竞争条件。

在第二篇文章《高效 CUDA 调试:使用 NVIDIA Compute Sanitizer 进行内存初始化和线程同步》中,我们探索了用于检查内存初始化和线程同步的工具。

在这篇文章中,我们重点介绍了 Compute Sanitizer 的一些其他功能,即它与 NVIDIA 工具扩展 (NVTX) 的集成,用于标记代码以方便更直接地使用 Compute Sanitizer。 我们还讨论了 Compute Sanitizer 本身的 API,以便创建更多用于调试 CUDA 应用程序的工具。

在这篇文章中,我们重点介绍了 Compute Sanitizer 的一些其他功能,即它与 NVIDIA 工具扩展 (NVTX) 的集成,用于标记代码以方便更直接地使用 Compute Sanitizer。 我们还讨论了 Compute Sanitizer 本身的 API,以便创建更多用于调试 CUDA 应用程序的工具。

NVIDIA Compute Sanitizer

Compute Sanitizer 是一套工具,可以对代码的功能正确性执行不同类型的检查。 主要有四种工具:

  • memcheck:内存访问错误和泄漏检测。
  • racecheck:共享内存数据访问危险检测工具。
  • initcheck:未初始化设备全局内存访问检测工具。
  • synccheck:线程同步危险检测。

除了这些工具之外,NVIDIA Compute Sanitizer 还具有更多功能:

  • 一个 API,用于创建针对 CUDA 应用程序的清理和跟踪工具
  • 与 NVIDIA 工具扩展 (NVTX) 集成
  • Coredump 支持与 CUDA-GDB 一起使用
  • 用于管理工具输出的抑制功能

将 Compute Sanitizer 与 NVTX 结合使用

NVTX 是一个基于 C 的 API,用于注释程序中的代码范围、事件和资源。 此注释允许在应用程序运行时收集更多信息,这可用于在分析和分析代码时改进数据表示。 Compute Sanitizer 和 NVTX 之间的集成使您能够使用 NVTX 来注释代码,以帮助 Compute Sanitizer 捕获错误。

有关 NVTX 注释的更多信息,请参阅以下帖子:

  • C/C++ 和 NVTX:CUDA 专业提示:使用 NVTX 生成自定义应用程序配置文件时间线
    Python 和 NVTX:NVIDIA 工具扩展 API:用于分析 Python 和 C/C++ 代码的注释工具
    Fortran 和 NVTX:使用 NVTX 自定义 CUDA Fortran 分析

NVTX 内存 API 使 CUDA 程序能够向 Compute Sanitizer 通知内存限制,例如内存池管理或权限限制以及内存标签。

内存池管理

NVTX 与 Compute Sanitizer 集成的第一个示例来自 NVTX 内存 API 的子分配部分。

使用 API,您可以将内存分配注释为内存池。 Compute Sanitizer 知道这些池,并且可以检测实际使用特定分配的哪些部分。 然后,如果在代码执行期间访问内存池的任何未注册部分,则通过 Compute Sanitizer memcheck 工具检测到这些访问。

这是基本内存池的示例 mempool_example.cu。

#include <stdio.h>
 
__global__ void populateMemory(int* chunk) {
  int i = threadIdx.x + blockDim.x * blockIdx.x;
  chunk[i] = i;
}
 
int main(int argc, char **argv) {
  int poolSize   = 4096 * sizeof(int);
  int numThreads = 63;
  // int bucketSize = numThreads * sizeof(int); // You need this later ...
 
  void *pool;
  cudaMallocManaged(&pool, poolSize); // Create your memory pool
 
  // Assign part of the memory pool to the bucket
  auto bucket = (int *)pool + 16; // Address of bucket is 16 bytes into the pool
 
  // Set values in bucket
  populateMemory<<<1, numThreads>>>(bucket);
  cudaDeviceSynchronize();
  printf("After populateMemory 1: bucket 0, 1 ..  62: %d %d .. %d\n", bucket[0], bucket[1], bucket[numThreads-1]);
 
  // Set some more values in bucket
  populateMemory<<<1, numThreads + 1>>>(bucket);
  cudaDeviceSynchronize();
  printf("After populateMemory 2: bucket 0, 1 ..  63: %d %d .. %d\n", bucket[0], bucket[1], bucket[numThreads]);
 
  cudaFree(pool);
  exit(0);
}

在代码示例中,您创建了一个大小为 4096 个整数的内存池(称为池!)。 然后,您分配该池的一部分(由变量存储桶标记),从距池开头 16 字节的地址开始。

您打算将存储桶设置为具有 numThreads 个元素,在本例中为 63,由变量bucketSize 确定。 然后,您可以使用 GPU 内核 populateMemory 用一些值填充存储桶。 块数设置为 1,线程数设置为 numThreads,这意味着 populateMemory 执行 1×63 次,按预期设置存储桶中的每个值。

但是,您随后尝试通过 populateMemory 内核再次填充存储桶。 这次,您将线程数设置为 numThreads+1 (64)。 您的意图是让存储桶具有 63 个值,但现在您尝试分配 64 个值。但是,这不会导致错误。 例如,您没有访问任何越界内存,因为存储桶所属的实际内存池足够大,可以容纳额外的元素。

编译它并通过 memcheck 运行它,以确认这个潜在的错误没有被发现。 我们在 NVIDIA V100 GPU 上运行,因此我们将 GPU 架构设置为 sm_70。 您可能需要更改此设置,具体取决于您运行的内容。

$ nvcc -o mempool.exe mempool_example.cu -arch=sm_70
$ ./mempool.exe
After populateMemory 1: bucket 0, 1 ..  62: 0 1 .. 62
After populateMemory 2: bucket 0, 1 ..  63: 0 1 .. 63
 
$compute-sanitizer --tool memcheck ./mempool.exe
========= COMPUTE-SANITIZER
After populateMemory 1: bucket 0, 1 ..  62: 0 1 .. 62
After populateMemory 2: bucket 0, 1 ..  63: 0 1 .. 63
========= ERROR SUMMARY: 0 errors

这就是 NVTX API 可以提供帮助的地方。 您可以使用 NVTX 的内存堆寄存器函数 nvtxMemHeapRegister 注册任何 cudaMalloc 内存分配。 这将内存注册为一个堆,代表可以进一步细分为多个区域的内存范围。 您可以通过以下过程在此代码中执行此操作。

首先,完成将 NVTX 与 Compute Sanitizer 结合使用所需的四个步骤。

对于 C 和 C++,NVTX 是一个仅包含头文件的库,没有依赖项,因此您必须获取 NVTX 头文件才能包含。 通常,这些会随您首选的 CUDA 下载一起提供,例如工具包或 HPC SDK。 但是,NVTX 内存 API 相对较新,因此现在可以从 /NVIDIA/NVTX GitHub 存储库获取它。 将来,它将作为工具包的一部分包含在内。

特别是,nvToolsExtMem.h 标头尚无法通过其他方法获得,因此在克隆 NVTX GitHub 分支 dev-mem-api 后检查它是否存在:

$ git clone --branch dev-mem-api https://github.com/NVIDIA/NVTX.git
…
$ ls NVTX/c/include/nvtx3/
nvToolsExtCuda.h    nvToolsExt.h           nvToolsExtMem.h     nvToolsExtSync.h  nvtxDetail
nvToolsExtCudaRt.h  nvToolsExtMemCudaRt.h  nvToolsExtOpenCL.h  nvtx3.hpp         nvtxExtDetail

现在您可以在源代码的开头包含 NVTX 和 NVTX API 头文件:

#include <nvtx3/nvToolsExt.h>
#include <nvtx3/nvToolsExtMem.h>

Compute Sanitizer 要求在任何 NVTX 调用之前初始化 CUDA 运行时。 无论如何,这可能会发生在您的代码中,具体取决于您开始使用 NVTX 的位置,但您可以使用 cudaFree 强制执行它,例如:

// Forces CUDA runtime initialization.
cudaFree(0);

最后,创建一个 NVTX 域。 这些是调用 API 所必需的。 目前,这些域没有特定的功能,但将用于未来的 Compute Sanitizer 版本。

// Create the NVTX domain
auto mynvtxDomain = nvtxDomainCreateA("my-domain");

好的,这就是完成的先决步骤。 现在,使用 NVTX 将池分配注册为内存池或堆:

nvtxMemVirtualRangeDesc_t myPoolRangeDesc = {}; // Descriptor for the
                                                // range memory pool
myPoolRangeDesc.size = poolSize; // Size of the range memory pool
myPoolRangeDesc.ptr  = pool;     // Pointer to the pool itself
 
nvtxMemHeapDesc_t myHeapDesc = {}; // Descriptor for the heap
 
myHeapDesc.extCompatID = NVTX_EXT_COMPATID_MEM;
myHeapDesc.structSize = sizeof(nvtxMemHeapDesc_t);
myHeapDesc.usage = NVTX_MEM_HEAP_USAGE_TYPE_SUB_ALLOCATOR;
myHeapDesc.type = NVTX_MEM_TYPE_VIRTUAL_ADDRESS;
myHeapDesc.typeSpecificDescSize = sizeof(nvtxMemVirtualRangeDesc_t);
myHeapDesc.typeSpecificDesc = &myPoolRangeDesc;
 
auto mynvtxPool = nvtxMemHeapRegister(mynvtxDomain, &myHeapDesc);

这些步骤已注册该池并将其分配给变量 mynvtxPool。 要在前面的示例中使用它,您现在必须在池中创建一个子分配来代表存储桶。 语法与分配池本身的方式没有什么不同,但这次使用区域描述符而不是堆描述符:

nvtxMemVirtualRangeDesc_t mySubRangeDesc = {}; // Descriptor for the range
mySubRangeDesc.size = bucketSize; // Size of your suballocation (in bytes)
mySubRangeDesc.ptr  = bucket;     // Pointer to the suballocation
 
nvtxMemRegionsRegisterBatch_t myRegionsDesc = {};
myRegionsDesc.extCompatID = NVTX_EXT_COMPATID_MEM;
myRegionsDesc.structSize  = sizeof(nvtxMemRegionsRegisterBatch_t);
myRegionsDesc.regionType  = NVTX_MEM_TYPE_VIRTUAL_ADDRESS;
myRegionsDesc.heap = mynvtxPool; // The heap you registered earlier
myRegionsDesc.regionCount = 1;
myRegionsDesc.regionDescElementSize = sizeof(nvtxMemVirtualRangeDesc_t);
myRegionsDesc.regionDescElements = &mySubRangeDesc;
 
nvtxMemRegionsRegister(mynvtxDomain, &myRegionsDesc);

这既是您的内存池,又是现在已向 NVTX 注册的子分配存储桶。 这意味着 Compute Sanitizer 可以将它们的属性作为其检查的一部分。 现在,看看它是否检测到将存储桶填充到其预期范围之外的错误尝试。

以下是现在具有 NVTX 注册的基本内存池的完整代码示例,包括 NVTX 注册,mempool_nvtx_example.cu

#include <nvtx3/nvToolsExt.h>
#include <nvtx3/nvToolsExtMem.h>
 
#include <stdio.h>
 
__global__ void populateMemory(int* chunk) {
  int i = threadIdx.x + blockDim.x * blockIdx.x;
  chunk[i] = i;
}
 
int main(int argc, char **argv) {
  int poolSize   = 4096 * sizeof(int);
  int numThreads = 63;
  int bucketSize = numThreads * sizeof(int);
 
  // Forces CUDA runtime initialization.
  cudaFree(0);
 
  // Create the NVTX domain
  auto mynvtxDomain = nvtxDomainCreateA("my-domain");
 
 
  void *pool;
  cudaMallocManaged(&pool, poolSize); // Create your memory pool
 
  // Register the pool with NVTX
  nvtxMemVirtualRangeDesc_t myPoolRangeDesc = {}; // Descriptor for the
                                                  // range memory pool
  myPoolRangeDesc.size = poolSize; // Size of the range memory pool
  myPoolRangeDesc.ptr  = pool;     // Pointer to the pool itself
 
  nvtxMemHeapDesc_t myHeapDesc = {}; // Descriptor for the heap
 
  myHeapDesc.extCompatID = NVTX_EXT_COMPATID_MEM;
  myHeapDesc.structSize = sizeof(nvtxMemHeapDesc_t);
  myHeapDesc.usage = NVTX_MEM_HEAP_USAGE_TYPE_SUB_ALLOCATOR;
  myHeapDesc.type = NVTX_MEM_TYPE_VIRTUAL_ADDRESS;
  myHeapDesc.typeSpecificDescSize = sizeof(nvtxMemVirtualRangeDesc_t);
  myHeapDesc.typeSpecificDesc = &myPoolRangeDesc;
 
  auto mynvtxPool = nvtxMemHeapRegister(mynvtxDomain, &myHeapDesc);
 
  // Assign part of the memory pool to the bucket
  auto bucket = (int *)pool + 16; // Address of bucket is 16 bytes into the pool
 
  // Register bucket as a suballocated region in NVTX
  nvtxMemVirtualRangeDesc_t mySubRangeDesc = {}; // Descriptor for the range
  mySubRangeDesc.size = bucketSize; // Size of your suballocation (in bytes)
  mySubRangeDesc.ptr  = bucket;     // Pointer to the suballocation
 
  nvtxMemRegionsRegisterBatch_t myRegionsDesc = {};
  myRegionsDesc.extCompatID = NVTX_EXT_COMPATID_MEM;
  myRegionsDesc.structSize  = sizeof(nvtxMemRegionsRegisterBatch_t);
  myRegionsDesc.regionType  = NVTX_MEM_TYPE_VIRTUAL_ADDRESS;
  myRegionsDesc.heap = mynvtxPool; // The heap you registered earlier
  myRegionsDesc.regionCount = 1;
  myRegionsDesc.regionDescElementSize = sizeof(nvtxMemVirtualRangeDesc_t);
  myRegionsDesc.regionDescElements = &mySubRangeDesc;
 
  nvtxMemRegionsRegister(mynvtxDomain, &myRegionsDesc);
 
  // Set values in bucket
  populateMemory<<<1, numThreads>>>(bucket);
  cudaDeviceSynchronize();
  printf("After populateMemory 1: bucket 0, 1 ..  62: %d %d .. %d\n", bucket[0], bucket[1], bucket[numThreads-1]);
 
  // Set some more values in bucket
  populateMemory<<<1, numThreads + 1>>>(bucket);
  cudaDeviceSynchronize();
  printf("After populateMemory 2: bucket 0, 1 ..  63: %d %d .. %d\n", bucket[0], bucket[1], bucket[numThreads]);
 
  cudaFree(pool);
  exit(0);
}

编译它并再次通过 Compute Sanitizer 运行它。 编译步骤中的 include 语句应指向您安装 NVTX 标头的位置。

$ nvcc -I ./NVTX/c/include -o mempool_nvtx.exe mempool_nvtx_example.cu -arch=sm_70
$ compute-sanitizer --tool memcheck --destroy-on-device-error=kernel ./mempool_nvtx.exe
========= COMPUTE-SANITIZER
After populateMemory 1: bucket 0, 1 ..  62: 0 1 .. 62
========= Invalid __global__ write of size 4 bytes
=========     at populateMemory(int *)+0x70
=========     by thread (63,0,0) in block (0,0,0)
=========     Address 0x7f2a9800013c is out of bounds
=========     and is 1 bytes after the nearest allocation at 0x7f2a98000040 of size 252 bytes
=========     Saved host backtrace up to driver entry point at kernel launch time
. . .
=========
After populateMemory 2: bucket 0, 1 ..  63: 0 1 .. 0
========= ERROR SUMMARY: 1 error

Compute Sanitizer 确实捕获了写入指定数组末尾的元素的尝试:

Invalid __global__ write of size 4 bytes

现在,如果您有部分内存池需要调整大小甚至销毁怎么办? NVTX 内存 API 还提供了类似的方法来对其 NVTX 注册执行此操作。

要调整大小,请返回之前的池和存储桶示例。 如果您想要将存储桶的大小从 63 个元素调整为 64 个元素,请使用以下代码修改前面的代码示例,也会调整您的 NVTX 注册存储桶的大小以反映这一点:

// Resizing the sub-allocation within the memory pool
 // You reuse mySubRangeDesc from earlier
 mySubRangeDesc.size = bucketSize + 4; // You want one extra int (4B) element
 mySubRangeDesc.ptr  = bucket;
 
 nvtxMemRegionsResizeBatch_t myNewRegionsDesc = {};
 myNewRegionsDesc.extCompatID = NVTX_EXT_COMPATID_MEM;
 myNewRegionsDesc.structSize = sizeof(mySubRangeDesc);
 myNewRegionsDesc.regionType = NVTX_MEM_TYPE_VIRTUAL_ADDRESS;
 myNewRegionsDesc.regionDescCount = 1;
 myNewRegionsDesc.regionDescElementSize = sizeof(mySubRangeDesc);
 myNewRegionsDesc.regionDescElements = &mySubRangeDesc;
 
 nvtxMemRegionsResize(mynvtxDomain, &myNewRegionsDesc);

正如您所看到的,它与子分配的初始声明类似,但最后使用了函数 nvtxMemRegionsResize。

仔细检查 Compute Sanitizer 是否对现在为调整大小的存储桶分配值的合法尝试感到满意。 在示例代码中两次调用populateMemory之间添加调整大小注册代码,然后编译运行。

$ nvcc -I./NVTX/c/include -o mempool_resize.exe mempool_resize_example.cu -arch=sm_70
$ compute-sanitizer --tool memcheck --destroy-on-device-error=kernel ./mempool_resize.exe
========= COMPUTE-SANITIZER
After populateMemory 1: bucket 0, 1 ..  62: 0 1 .. 62
After populateMemory 2: bucket 0, 1 ..  63: 0 1 .. 63
========= ERROR SUMMARY: 0 errors

希望您能看到类似此示例的内容。 当您调整了子分配注册的大小后,现在不再有关于尝试访问添加到末尾的新元素的投诉。

需要相当多的代码行来注册和调整池和子分配的大小,以便将它们与 NVTX 和 Compute Sanitizer 一起使用。 对于更复杂的代码来说,这可能会变得很麻烦,因此将这些步骤封装到一个单独的类中可能会很有用。 很方便,/NVIDIA/compute-sanitizer-samples GitHub 存储库中提供了这种方法的一个很好的示例,因此这是您自己的代码的一个很好的起点。

还有两个 NVTX API 需要提及:

  • 命名 API:使区域或子分配具有与其关联的 ASCII 名称。 然后,它可以用于在错误报告中通过名称来引用分配,目前支持泄漏和未使用的内存报告。
  • 权限 API:允许将分配访问权限限制为只读或原子等。

Compute Sanitizer API 用于创建您自己的工具

Compute Sanitizer 附带 API,使您能够创建自己的清理和跟踪工具来定位 CUDA 应用程序。 它是一组函数,可用于与 Compute Sanitizer 交互以进行控制和配置、启用或禁用其功能以及访问其结果。

该 API 还为您提供了一种将 Compute Sanitizer 集成到您的开发工作流程中的便捷方法,因为它可以轻松集成到现有的 CUDA 应用程序中。 借助 Compute Sanitizer API,您可以直接利用强大的调试功能并提高 CUDA 应用程序的可靠性和性能。

它由以下子 API 组成:

  • 回调:使您能够在用户代码中注册回调,其中回调可以与相关 CUDA 函数或事件组关联,例如 memcpy 操作或驱动程序函数。 然后订阅者可以使用这些回调,例如用于事件跟踪。
  • 修补:允许加载修补函数并将其插入到 GPU 上执行的设备代码中。 然后,它们可以用作检测点,这意味着每当执行修补事件时都会执行修补函数,例如,设置回调,例如进行内存访问的设备代码。
  • 内存:提供标准 CUDA 内存 API 的替换函数。 可以从 Compute Sanitizer 回调中安全地调用替换,例如使用替换 sanitizerAlloc 而不是 cudaMalloc。

结合起来,这些 API 使您能够将 Compute Sanitizer 功能合并到您自己的工具中。

有关更多信息和一些示例代码,请参阅 NVIDIA Compute Sanitizer API 指南。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C#全新一代医院手术麻醉系统围术期全流程源码

目录 一、麻醉学科的起源 二、麻醉前访视与评估记录单 患者基本信息 临床诊断 患者重要器官功能及疾病情况 病人体格情况分级 手术麻醉风险评估 拟施麻醉方法及辅助措施 其他需要说明的情况 访视麻醉医师签名 访视时间 与麻醉相关的检查结果 三、手术麻醉信息系统…

Laravel扩展包的开发

扩展包的开发 1. 创建一个新项目&#xff0c;初始化扩展包配置 首先创建一个全新的Laravel项目&#xff1a; composer create-project --prefer-dist laravel/laravel laravelPkg 接下来&#xff0c;在项目中创建目录package/{your_name}/{your_package_name} mkdir -p pa…

STM32硬件I2C通信外设

文章目录 前言I2C硬件介绍10 位地址模式硬件I2C的引脚定义I2C框图主机发送序列图主机接收序列图 硬件I2C读写MPU6050总结 前言 本文主要介绍stm32自带的I2C通信外设&#xff0c;对比与软件模拟I2C&#xff0c;硬件I2C可以自动生成时序&#xff0c;时序的操作更加及时规范&…

什么是网页抓取 Web Scraping?如何进行网页抓取?

现在&#xff0c;不论是个人开发者还是庞大的企业都需要从互联网抓取大量数据&#xff0c;而网页抓取&#xff08;Web Scraping&#xff09;技术正是获取互联网上无尽信息宝库的一把钥匙。通过网页抓取工具&#xff0c;我们可以快速收集产品价格、市场趋势、用户评论等关键数据…

uniapp h5 touch事件踩坑记录

场景&#xff1a;悬浮球功能 当我给悬浮球设置了 position: fixed; 然后监听悬浮球的touch事件&#xff0c;从事件对象中拿到clientY和clientX赋值给悬浮球的left和top属性。当直接赋值后效果应该是这样子&#xff1a; 注意鼠标相对悬浮球的位置&#xff0c;应该就是左上角&a…

力扣_876_ 链表的中间结点(c语言)

题目描述&#xff1a; 解题方法&#xff1a; struct ListNode* middleNode(struct ListNode* head) {struct ListNode* l1,*l2;l1l2head;while(l2&&l2->next){l1l1->next;l2l2->next->next;}return l1; }

element UI季度选择器的实现

效果展示 用elementUI的select实现季度选择器 代码实现 generateQuarterOption放在methods中&#xff0c;需要近几年的只需要修改第一个循环的次数即可&#xff0c;mounted生命周期函数中调用generateQuarterOption() generateQuarterOption() {//近3年所有季度let now ne…

6行代码,1行命令!轻松实现多模态(视觉)模型离线推理 在线服务

早在去年年底&#xff0c;LMDeploy 已经悄悄地支持了多模态&#xff08;视觉&#xff09;模型&#xff08;下文简称 VLM&#xff09;推理&#xff0c;只不过它静静地躺在仓库的 examples/vl 角落里&#xff0c;未曾与大家正式照面。 LMDeploy 开源链接&#xff1a; https://gi…

Android | 开发过程遇到的报错以及解决方法

注&#xff1a; 此博客为记录个人开发过程中遇到的报错问题以及解决方案。 由于不同版本环境等因素影响&#xff0c;解决方案对其他人可能无效。 本博客仅提供一种解决思路&#xff0c;具体问题请具体分析。 报错&#xff1a;Connection timed out: connect解决&#xff1a;在G…

763. 划分字母区间(力扣LeetCode)

763. 划分字母区间 题目描述 给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段&#xff0c;同一字母最多出现在一个片段中。 注意&#xff0c;划分结果需要满足&#xff1a;将所有划分结果按顺序连接&#xff0c;得到的字符串仍然是 s 。 返回一个表示每个字符串…

pip wheel直接为离线环境打包需要的python包

很多情况下&#xff0c;需要离线安装python库&#xff0c;直接下载所需的库包时&#xff0c;可能又要求更新或安装相关的依赖包&#xff08;这就非常麻烦了&#xff09;&#xff0c;所以推荐一条命令一步到位&#xff0c;命令如下&#xff1a; pip wheel -r requirements.txt …

设计模式-设配器模式

目录 &#x1f38a;1.适配器模式介绍 &#x1f383;2.适配器类型 &#x1f38f;3.接口适配器 &#x1f390;4.类的适配器 &#x1f38e;5.优缺点 1.适配器模式介绍 适配器模式&#xff08;Adapter Pattern&#xff09;是作为两个不兼容的接口之间的桥梁。这种类型的设…

什么?想让视频号小店领先同行,竟然这么简单!

大家好&#xff0c;我是电商小布。 视频号小店从推出到现在&#xff0c;逐渐也是被越来越多的人所熟知了。 虽然说当前市场内部的商家数量并不多&#xff0c;竞争力不大。 但是在入驻之后想要领先同行商家&#xff0c;产生更好的店铺数据&#xff0c;该怎么来做呢&#xff1…

学习JavaEE的日子 Day29 yield,join,线程的中断,守护线程,线程局部变量共享,线程生命周期

Day29 多线程 12. 线程的礼让 Thread.yield(); 理解&#xff1a;此方法为静态方法&#xff0c;此方法写在哪个线程中&#xff0c;哪个线程就礼让 注意&#xff1a;所谓的礼让是指当前线程退出CPU资源&#xff0c;并转到就绪状态&#xff0c;接着再抢 需求&#xff1a;创建两个…

多叉树题目:N 叉树的后序遍历

文章目录 题目标题和出处难度题目描述要求示例数据范围进阶 解法一思路和算法代码复杂度分析 解法二思路和算法代码复杂度分析 解法三思路和算法代码复杂度分析 题目 标题和出处 标题&#xff1a;N 叉树的后序遍历 出处&#xff1a;590. N 叉树的后序遍历 难度 3 级 题目…

Android笔记(三十):PorterDuffXfermode实现旋转进度View

背景 核心原理是使用PorterDuffXfermode Path来绘制进度&#xff0c;并实现圆角 效果图 Android笔记(三十)效果演示 进度条绘制步骤 将ImageView矩形七个点的坐标存储起来&#xff08;configNodes&#xff09; 他们对应着7个不同的刻度&#xff0c;每个刻度的值 i * &#…

Unity | 射线检测及EventSystem总结

目录 一、知识概述 1.Input.mousePosition 2.Camera.ScreenToWorldPoint 3.Camera.ScreenPointToRay 4.Physics2D.Raycast 二、射线相关 1.3D&#xff08;包括UI&#xff09;、射线与ScreenPointToRay 2.3D&#xff08;包括UI&#xff09;、射线与ScreenToWorldPoint …

计算机基础,挑战全网最全解析

1.什么是计算机&#xff1f; 2.冯诺依曼结构 3.进制 4.摩尔斯码和布莱叶盲文 摩尔斯码 布莱叶盲文

如何使用群晖WebDAV实现固定公网地址同步Zotero文献管理器

文章目录 前言1. Docker 部署 Trfɪk2. 本地访问traefik测试3. Linux 安装cpolar4. 配置Traefik公网访问地址5. 公网远程访问Traefik6. 固定Traefik公网地址 前言 Trfɪk 是一个云原生的新型的 HTTP 反向代理、负载均衡软件&#xff0c;能轻易的部署微服务。它支持多种后端 (D…