`

解剖一个有缺陷的微基准测试

 
阅读更多

原文:《Anatomy of a flawed microbenchmark

 

解剖一个有缺陷的微基准测试

前言

即使“良好的性能”不是一个项目的关键需求,甚至不是需求之一,你也很难忽略性能方面的考虑。因为你可能会认为不考虑性能的程序员不是好工程师。在通往编写高性能代码的过程中,开发人员经常会编写基准测试程序来测量对比不同实现方式的性能。不幸的是,正如《Dynamic compilation and performance measurement》所说,与其它静态编译语言相比,评估 Java 中一段代码或数据结构的性能要困难得多。

 

一个有缺陷的微基准测试

我十月份的文章《More flexible, scalable locking in JDK 5.0》发布后,一位同事给我发来一段基准测试代码 SyncLockTest(见下文“有缺陷的 SyncLockTest 微基准测试”),号称能测量出 synchronized 原语和新的 ReentrantLock 类两者哪个更快。在他的笔记本电脑上运行完这段测试代码后,他得出“synchronized 原语更快”这个与我文章相反的结论,而证据就是他的这次基准测试。然而他的整个基准测试过程中,微基准测试的设计、实现、执行以及对结果数据的解释都存在缺陷。这个同事是个非常聪明的人,他已经在这方面的微基准测试已经有较多经验了。这也反衬出微基准测试真的很难。

 

有缺陷的 SyncLockTest 微基准测试

interface Incrementer {
  void increment();
}

class LockIncrementer implements Incrementer {
  private long counter = 0;
  private Lock lock = new ReentrantLock();
  public void increment() {
    lock.lock();
    try {
      ++counter;
    } finally {
      lock.unlock();
    }
  }
}

class SyncIncrementer implements Incrementer {
  private long counter = 0;
  public synchronized void increment() {
    ++counter;
  }
}
 
class SyncLockTest {
  static long test(Incrementer incr) {
    long start = System.nanoTime();
    for(long i = 0; i < 10000000L; i++)
      incr.increment();
    return System.nanoTime() - start;
  }
 
  public static void main(String[] args) {
    long synchTime = test(new SyncIncrementer());
    long lockTime = test(new LockIncrementer());
    System.out.printf("synchronized: %1$10d\n", synchTime);
    System.out.printf("Lock:         %1$10d\n", lockTime);
    System.out.printf("Lock/synchronized = %1$.3f",
      (double)lockTime/(double)synchTime);
  }
}

SyncLockTest 定义了同一个接口的两种实现,并使用 System.nanoTime() 方法对每种实现的 10,000,000 次调用进行计时。这两种实现都是线程安全的计数器。其中一个使用了内建的同步(synchronized 原语),另一种使用了 ReentrantLock 类。这份基准测试代码的目标是为了回答“synchronized 原语和 ReentrantLock 类哪个更快”。让我们来分析一下为什么这份看上去没有错误的基准测试为什么无法达到它所声称的目标,或者说这份测试到底测了什么。

 

概念上的缺陷

先不谈实现上的缺陷,这份基准测试在概念上就有严重缺陷——它误解了它试图要解决的问题。它试图测量比对(内建)synchronization(synchronized 原语)和 ReentrantLock 的性能开销,也就是两者在协调多线程操作时所用技术的性能开销。但是这份测试代码只有一个线程,所以永远不会有竞争。它从一开始就遗漏了测试与锁有关(需要锁)的场景。

众所周知,在早期的 JVM 实现中,非竞争场景下的(内建)同步(synchronized 原语)比较慢。但是这性能问题现在基本上已得改善。(查看文末相关话题了解JVM优化非竞争场景下内建同步性能所使用的技术。)另一方面,竞争场景下(内建)同步的开销仍然比非竞争场景下要大得多。当发生锁竞争时,JVM 需要维护一个等待线程的队列,此外还需调用操作系统的方法对那些未能立即拿到锁的线程进行阻塞和解除阻塞操作。此外,处于高度竞争的应用往往伴随着低吞吐量,因为线程调度花费的时间更多,实际处理业务的时间少了,而且当线程被阻塞等待锁时可能导致 CPU 处于空闲状态。测试(内建)同步(synchronized 原语)的基准测试必须考虑到现实情况中的竞争程度。

 

方法上的缺陷

该基准测试设计在执行方面至少存在两处错误:

  • 它运行在单处理器系统中(注:是单线程模式)
    • 这并不是高并发程序运行的常规环境。在这样的环境中,同步操作的性能表现会和多处理器系统环境有本质上的不同。
  • 它只在一个平台上做过测试

当测试一个原语(尤其是与底层硬件交互如此重要)的性能时,在得出结论前,有必要在更多不同平台上测试。当测试并发这类复杂的场景时,建议在十个以上不同测试系统,不同处理器和不同数量的处理器场景中测试后,再得出总体的性能概论。(内存配置和处理器代次也是需要考虑的。)

 

实现上的缺陷

在实现方面,SyncLockTest 忽视了许多动态编译相关的特性。正如你在《Dynamic compilation and performance measurement》所看到的,HopSpot JVM 会先以解释模式执行一段代码(注:以方法为单位),只有当执行次数达到一定数量(阀值)时才会将其编译成机器码。如果没有正确对JVM进行预热,会在两方面严重影响性能测量。首先,JIT分析与编译代码的耗时被包含到了测试的运行时间内。更重要的是,如果编译发生在测试(正式测试代码)运行中,你的测试结果(耗时)将是部分解释运行的耗时的和,加上JIT编译耗时,加上优化后执行的耗时。这样的结果对于被测代码的真实性能没有多少有用的信息。另一方面,如果在运行(正式)测试前,代码没有被编译(编译成机器码)过,且在测试运行期间没有发生编译,那么你的整个测试过程都是解释执行的。这样的结果也无法给你多少对于被测代码在真实世界中性能的有用信息。

SyncLockTest 也受到了《Dynamic compilation and performance measurement》中所讨论的内联和反优化问题的影响。在那篇文章中,第一份耗时测量代码以单态调用转化的方式被激进地内联(优化)了。(单态调用转换是指针对虚方法的调用被转换为对目标方法的直接调用。Java中的方法默认是虚方法,是一种面向对象设计中的多态特性)第二份代码则因为随后JVM载入另一个继承自同一基类或接口的类而被“反优化”。当耗时测试方法来自一个 SyncIncrementer 实例时,(JVM)运行时识别出只有一个实现了 Incrementer 接口的类被载入,所以会将针对虚方法 increment() 的调用转换为对 SyncIncrementer 实例方法的直接调用。随后,当耗时测试方法来自一个 LockIncrementer 实例时,test() 方法被当作虚方法重新编译。这意味着第二份 test() 方法耗时测试中的每个迭代比第一份做了更多的工作。这好比我们是在比较苹果与橘子的区别。这会严重扭曲测试结果,导致无论哪种实现,第一个先运行的方案会显得更快。

 

基准测试代码看上去不像真实的代码

到目前为止所讨论的基准测试代码缺陷可以通过合理的返工修复。如,引入类似“竞争程度”这样的测试参数,并且在大量不同的系统上、不同测试参数值条件下测试。但是还有一些缺陷可能无法通过任何调整来解决。为了了解为什么会这样,你需要像JVM一样思考,并了解当 SyncLockTest 被编译时会发生什么。

Heisenbenchmark 原则

当编写一个微基准测试来测量像 synchronization 的(编程)语言原语性能时,你就是在和 Heisenberg 原则作斗争。你想要测量操作X有多快,所以你不想做除了X之外的任何其它操作。但是通常得到一个什么也不做的基准测试。在这样的基准测试中,编译器会在你没有意识到的情况下执行部分或全部优化,导致测试运行得比期望快。如果你将额外的代码Y加入到你的基准测试中,你测量的就是X+Y的性能,也就是对X的测量中中引入了“噪声”。更糟糕的是Y的存在改变了JIT优化X的行为。编写一个良好的微基准测试意味着在“(额外)操作与数据流依赖不足以阻止编译器优化你的整个测试程序”和“(额外)操作太过多导致你想测量的东西被‘噪声’淹没”之间找到一个难以捉摸的平衡。

因为运行时编译使用 profiling 数据来指导它的优化操作,JIT可能会更好地优化测试代码,而不同于真实代码。与所有基准测试一样,编译器有能力优化整个测试代码是一项重要的风险。因为它将意识到基准测试代码没有做任何事情,或者所产出的结果没有被用于任何操作。编写有效的基准测试需要让编译器变“愚”,使它不会去除这些“无效”代码(即使真的是无效的代码)。两个 Incrementer 类中计数变量的使用方式没能使编译器变“愚”。编译器在消除无效代码方面通常比我们认为的更聪明。

事实上这个问题与 synchronization 是语言内建特性复合在一起。JIT编译器被允许在处理同步代码块时有一些自由,来降低性能开销。在有些情况下,同步操作可以被完全移除,而且相邻同步代码块对同一个同步元(monitor)的同步操作可以被合并。如果我们在测量同步的开销,这些优化真的会打击到我们。因为我们不知道有多少(这个案例中几乎是所有)同步操作被优化掉了。更糟的是,JIT优化 SyncTest.increment() 中什么也没做的代码的方式与真实世界中的程序是非常不同的。

更糟的来了。这个微基准测试的表面目的是测试 synchronization(synchronized 原语) 和 ReentrantLock 哪个更快。因为 synchronization 是语言内建的,而 ReentrantLock 是一个普通的 Java 类,所以编译器对“什么也没做的 synchronization”和“获取 ReentrantLock”的优化是不同的。这个优化使得什么也没做的 synchronization 看起来更快。因为编译器对这两个测试案例的优化是不同的,在真实世界中场景中的优化方式也是不同的,所以这份测试程序的执行结果几乎不能能告诉我们两者(synchronization 和 ReentrantLock)在真实世界场景中的性能差异。

 

无效代码消除

在《Dynamic compilation and performance measurement》中,我论述了基准测试中无效代码消除的问题。这是因为基准通常不会对计算结果做任何处理,导致编译器通常可以基准测试中的整块代码,进而扭曲耗时测量。这份测试代码多个地方有该问题。事实上,编译器的无效代码消除对我们(的程序)不一定是致命的。但在该案例中,这个问题在两个代码执行路径上能导致不同程度的优化,系统性地扭曲我们的测量。

两个 Incrementer 类旨在做一些什么也不做的工作(对一个变量做递增)。但是一个聪明的JVM会察觉到这两个计数变量从来都不会被读取,因此可以消除相关代码(包括递增操作代码)。这就是我们存在严重问题的地方——现在 SyncIncrementer.increment() 中的 synchronized 代码块是空的,编译器可以将它完整移除,而 LockIncrementer.increment() 中仍然有锁相关的代码,编译器可能无法完全消除。你可能会认为这(就)是 synchronization 的一种优势,即,编译器能更容易地消除它。但是这种现象更多得是出现在什么也没做的基准测试代码中,而不是真实世界中编写良好的代码。(也就是说真实应用场景中,我们几乎不会写这些什么没做的代码)

问题就是,编译器优化会更好地优化其中一种实现方式,但是这个差异只存在于什么也没做的基准测试中。这导致这种比较 synchronization 和 ReentrantLock 性能的方法会如此之难。

 

循环展开与锁合并

即使编译器没有消除对计数变量的操作,它依然会以不同的方式优化两个 increment() 方法。一种标准的优化是 循环展开。编译器会展开循环代码,从而减少代码分支数量。被展开的迭代数量取决于循环体内代码的数量。很明显,LockIncrementer.increment() 方法中循环体内的代码比 SyncIncrementer.increment() 方法中的多。进一步说,当 SyncIncrementer.incrementer() 被展开且方法调用被内联,这个被展开的循环就是一个“加锁——增值——解锁”操作组合的序列。因为所有这些加锁操作都是针对同一个同步元(monitor),编译器可以执行锁合并(lock coalescing 或 lock coarsening)来合并相邻的同步代码块。这意味着 SyncIncrementer 执行的同步操作比期望的更少。(更糟的是,加锁操作被合并后,同步体内将只包含一个“增值”操作序列,其消耗仅相当于单次加法操作。而且,如果该操作被重复应用,整个循环将被折叠为单个同步块,内部操作就是单个“counter=10000000”操作。真实世界中的JVM真的能执行这些优化。)

再次说明,问题不仅仅是(编译器)优化器会优化我们的基准测试,而是它对其中一种实现方式所做的优化程度与另一种实现方式不同,且对任何一种实现方式所采用的优化方法与真实世界中的不一样。

 

缺陷计分卡(清单)

这些是此基准测试未达到其创建者目的的原因(这是不详尽的清单):

  • 没有执行预热,而且没有考虑JIT执行所花的时间
  • 此测试容易受到单态调用转换和随后的反优化影响
  • synchronized 块和 ReentrantLock 所包含的代码块是无效代码,这会扭曲JIT优化它们的方式;它可以消除整个 synchronization 测试
  • 该测试程序想测量锁的性能,但是它没有包含(多线程)竞争的影响,而且它只在一个单处理系统上运行
  • 该测试程序没有在足够多的不同平台上运行
  • 编译器能在 synchronization 上执行的优化比 ReentrantLock 上更多,但是这些优化方式并不会对真实世界中使用 synchronization 的程序有多大帮助

问错误的问题,得到错误的答案

微基准测试的可怕之处是它们总是产出一个数字,即使这个数字是毫无意义的。它们确实测量一些东西,只是我们不知道到底是什么。通常,它们只测量特定微基准测试的性能,不做其它事。但是很容易说服你自己你的基准测试测量的是某个特定的(数据)结构,并错误地得出对那个(数据)结构的性能结论。

即使当你编写了一个优秀的基准测试,你的测量结果也可能只在你执行的系统上有效。如果你在一个单处理器且内存较小的笔记本系统上运行,你可能无法得出任何关于它在服务器系统上的性能结论。底层并发原语(如 compare-and-swap)的性能在不同硬件架构系统上的性能表现会有相当大的不同。

事实时,试图通过单个数字来测量类似“synchronization 性能”这样的目标是不可能的。synchronized 的性能在不同JVM、处理器、工作负荷、JIT活动、处理器数量、被同步代码数量等条件下都会不同。你能做得最好的就是在一系列不同的平台上运行一系列基准测试,并寻找结果中的相似性。只有那时你才可以开始对 synchronization 的性能下结论。

在 JSR 166(java.util.concurrent)测试过程的基准测试中,不同平台上的性能曲线的形状非常不同。硬件结构操作(construct)(如,CAS)的性能在不同平台和不同处理器数量的场景中表现不同(如,在单处理器系统中,CAS永远不会失败)。内存屏障的性能在单个 Intel P4 超线程(一个芯片两个处理器核心)场景下比两个 P4 要快。而且这个两种场景下的性能都不同于与 Sparc 处理器。所以你能做得最好的就是尝试构建“典型”的(基准测试)样例并在“典型”的硬件环境中测试,并期望这会产出一些与我们的真实程序在真实硬件上性能表现相关的“领悟”。“典型”(基准测试)的样例有哪个些构成要素?一个融合了计算、IO、同步、竞争、内存局部性、分配行为、上下文切换、系统调用、线程间通信的基准测试才会近似与真实世界的应用。也就是说,现实的基准测试看上去非常像一个真实世界的程序。

 

如何编写一个完美的微基准测试

所以,你如何才能写出一个完美的微基准测试呢?首先,编写一个优化良好的JIT。与那些已经编写其它优化良好的JIT的人见面(他们很容易找,因为没有优化良好的JIT不多)。邀请他们吃晚餐,并交换关于如何使Java字节码运行更快的性能伎俩故事。阅读几百篇关于优化 Java 代码执行的论文,并且写几篇。那时候你将拥有编写测量某种东西开销的良好准测试的能力,像同步、对象池、虚方法调用。

 

你在开玩笑吗?

你可能会认为上述编写良好微基准测试的秘诀过于保守。但是编写良好的微基准测试确实需要动态编译、优化和 JVM 实现技术方面的大量知识。为了编写一个真的会测试你所想测试对象的测试程序,你不得不理解“编译器将会做何操作”、“动态编译所得代码的性能特征”、“测试代码与典型的真实世界代码在使用相同(数据)结构上有何不同”。没有这个了解程度,你将无法得知你的测试程序是否测量了你所想要的东西。

 

所以你该做什么?

如果你真的想知道 synchronization 是否比替代的锁机制快(或者其它类似的微性能问题),你该做什么?一个观点是“相信专家”(这多大多数开发者来说都不太合适)。在 ReentrantLock 类的开发中 JSR 166 EG 的成员在很多不同平台上运行了即使没有上千也有几百小时的性能测试,检查了JIT编译出来的机器码,仔细钻研了测试结果。然后他们微调了代码重来。在开发和研究这些类时运用了大量关于JIT和微处理器行为的经验与详细理解。但还是很不幸地无法基于单个基准测试程序结果得出总结,尽管我们非常希望能够得出结论。另一个观点是把你的注意力集中于“宏观”的基准测试。即,编写一些真实世界的程序,用两种方式都实现一遍,开发一套现实的负载生成策略,并分别测量你的程序在现实的负载条件和现实的部署配置下两种不同实现方式的性能。这是大量的工作,但是它将会使你更接近于你要寻找的答案。

 

相关话题

 

分享到:
评论

相关推荐

    东华大学在北京2021-2024各专业最低录取分数及位次表.pdf

    全国各大学2021-2024在北京各专业录取分数及最低位次

    中国矿业大学(北京)在广东2021-2024各专业最低录取分数及位次表.pdf

    全国各大学在广东省2021~2024年各专业最低录取分数及位次

    NJUST软件课程设计.zip

    NJUST软件课程设计.zip

    使用Python语言来模拟一个中秋节日的庆祝活动

    使用Python语言来模拟一个中秋节日的庆祝活动,其中包含了节日祝福的发送、一个小型的灯笼制作游戏(以文本形式展示),以及一个基于控制台的“赏月”体验。 在这个示例中,print_lantern函数用于打印一个简单的灯笼图案到控制台,send_mid_autumn_greetings函数发送中秋节日的祝福,play_lantern_game函数模拟了一个简单的灯笼制作游戏(由于文本界面的限制,这里仅进行了模拟),而moon_viewing_experience函数则提供了一段基于控制台的“赏月”体验。 当然,这只是一个非常基础的示例。在实际应用中,你可以根据需要添加更复杂的游戏逻辑、图形用户界面(GUI)、网络功能(如在线发送祝福)等。此外,你也可以使用其他编程语言(如JavaScript、Java、C#等)来实现类似的功能,具体取决于你的应用场景和开发环境。

    1565-基于51单片机的数码管热电偶温度报警(数码管,热电偶,单阀值)proteus、原理图、流程图、物料清单.zip

    1565-基于51单片机的数码管热电偶温度报警(数码管,热电偶,单阀值)proteus、原理图、流程图、物料清单 资料介绍: 工业控制系统中经常使用到温度参数测量,由单片机构成的 温度测量显示电路是此类电路中的常用构成形式,测量指标如下: 1 .要求测量的温度在0〜400℃ 2 .测量精度为0.1℃ 3 .用四位LED数码管显示测量结果 4 .具有必要的按键,按键形式自定 5 .扩展功能:声音提示 有哪些资料: 1、仿真工程文件 2、源代码工程文件 3、原理图工程文件 4、功能介绍 5、元件清单

    C++代码注释的版本控制:自动化策略与实

    代码注释是软件开发中的关键组成部分,它不仅帮助开发者理解代码的功能和结构,还便于维护和后续开发。在C/C++项目中,将代码注释纳入版本控制是确保注释与代码同步更新的重要手段。本文将详细介绍如何在C/C++项目中实现代码注释的版本控制,探讨不同的版本控制策略、工具选择、配置方法以及如何通过代码实现自动化注释版本控制。 代码注释的版本控制对于维护项目的健康和可维护性至关重要。通过使用Git等版本控制工具,并结合自动化脚本和工具,可以有效地管理注释的版本。本文介绍了版本控制的重要性、工具选择、自动化注释版本控制的方法和代码示例,为C/C++项目中的注释版本控制提供了实用的指导。随着开发工具和技术的不断进步,自动化注释版本控制将在未来发挥更大的作用。

    嘉应学院在广东2021-2024各专业最低录取分数及位次表.pdf

    全国各大学在广东省2021~2024年各专业最低录取分数及位次

    北京城市学院在北京2021-2024各专业最低录取分数及位次表.pdf

    全国各大学2021-2024在北京各专业录取分数及最低位次

    【目标检测数据集】水稻病害检测数据集11960张VOC+YOLO格式.zip

    数据集格式:Pascal VOC格式+YOLO格式(不包含分割路径的txt文件,仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数):11963 标注数量(xml文件个数):11963 标注数量(txt文件个数):11963 标注类别数:12 标注类别名称:["BLBD","BLSD","BSD","DPD","FSD","Healty","NBD","NBSD","Non-Rice","RBD","RRSD","SBD"] 每个类别标注的框数: BLBD 框数 = 1030 BLSD 框数 = 1067 BSD 框数 = 1064 DPD 框数 = 1465 FSD 框数 = 2154 Healty 框数 = 1266 NBD 框数 = 1412 NBSD 框数 = 975 Non-Rice 框数 = 1298 RBD 框数 = 1070 RRSD 框数 = 972 SBD 框数 = 1303 总框数:15076 使用标注工具:labelImg 标注规则:对类别进行画矩形框

    使用Vcpkg和Conan管理C-C++项目的依赖包.md

    在C/C++开发中,管理依赖包是关键的任务。本文深入探讨了两种流行的包管理工具:Vcpkg和Conan。Vcpkg,由Microsoft开发,专注于简化跨平台C/C++库的安装与管理,特别是与CMake和Visual Studio的集成。Conan则是一个开源工具,支持多种构建系统,并提供高度的自定义能力。文章详细介绍了这两个工具的安装、使用、配置和高级功能,包括如何通过它们管理库依赖、支持不同的构建配置、创建自定义包等。通过案例分析,展示了如何在实际项目中应用Vcpkg和Conan,比较了它们的设计理念和功能差异。掌握这些工具和技巧,能够显著提升项目的开发效率和依赖管理的灵活性。

    【C#绘图进阶】实时曲线绘制+精准坐标轴,让你的数据可视化更加专业!

    在数据驱动的时代,如何将复杂的数据转化为直观易懂的图表,是每个开发者都需要掌握的技能。今天,我们为你带来C#实时曲线绘制带坐标轴的全面教程,让你的数据可视化项目更上一层楼! 为什么选择C#? 强大功能:C#作为微软主推的编程语言,拥有强大的.NET框架支持,能够轻松实现高性能的图形界面和数据处理。 广泛应用:从桌面应用到Web开发,C#的身影无处不在,掌握它意味着你能在多个领域游刃有余。 高效绘图:结合GDI+、System.Drawing或更高级的图形库如OxyPlot、LiveCharts等,C#能够绘制出精美且高效的实时曲线图。 教程亮点: 从零到一:从创建基本的WinForms或WPF应用程序开始,逐步引导你搭建绘图框架。 实时更新:详细讲解如何捕获数据并实时更新图表,让你的曲线图始终与数据源保持同步。 坐标轴定制:教你如何调整坐标轴的刻度、标签、网格线等,确保图表既美观又准确。 性能优化:分享实用的性能优化技巧,确保在高数据量下也能流畅绘制曲线图。 你将学到: C#基础与图形界面编程入门 实时数据获取与处理 GDI+或第三方图形库的使用 坐标轴设

    PR与PI双环控制单相PWM整流器 MATLAB仿真模型 simulink (1)基于比例谐振控制的单相PWM整流器MATLAB

    PR与PI双环控制单相PWM整流器 MATLAB仿真模型 simulink (1)基于比例谐振控制的单相PWM整流器MATLAB仿真模型; (2)电压、电流双闭环控制,电压环采用Pl,电流环采用PR,实现电流完美跟踪; (3)调制策略采用SPWM; (4)输入电压电流同相位,仿真功率因数大于0.9999,接近1;(5)输入电流低谐波,仿真谐波含量0.97%,<1 (6)仿真工况为输入电压AC220V,输出电压DC400v,负载10kW;(7)仿真模型带参考lunwen。

    徐州工程学院在广东2021-2024各专业最低录取分数及位次表.pdf

    全国各大学在广东省2021~2024年各专业最低录取分数及位次

    letex教程汉化版,可以直接下载观看

    letex教程汉化版,可以直接下载观看

    QTableWidget重绘,实现合并单元格

    1 . void QTableView::​setColumnWidth(int column, int width) 设置某一列的列宽 2. 通过QHeaderView * QTableView::​horizontalHeader() const获得QHeaderView 在调用QHeaderView里面的函数进行设置 3. QHeaderView的列宽设置函数 3.1 设置限制值 设置列宽最大值:void setMaximumSectionSize(int size) 设置列宽最小值:void setMinimumSectionSize(int size) 3.2 设置自动适应 设置某一列的自动适应模式:void QHeaderView::​setSectionResizeMode(int logicalIndex, ResizeMode mode) 设置整个表头的自适应模式:void setSectionResizeMode(ResizeMode mode)

    整蛊朋友的小网页,源代码见arcxingye大佬的吃掉小鹿乃项目httpsgithub.com

    整蛊朋友的小网页,源代码见arcxingye大佬的吃掉小鹿乃项目httpsgithub.com_JRY2001.github.io

    安庆师范大学在安徽2021-2024各专业最低录取分数及位次表.pdf

    全国各大学在广东省2021~2024年各专业最低录取分数及位次

    软件工程课程设计-爱篮球论坛-Android端.zip

    软件工程课程设计-爱篮球论坛-Android端.zip

    RRU5516e供配电方案.pptx

    1.当模块实际功率在1100W以内,可以正常工作整体影响较小;当模块实际功率大于1100W时,最大电流超过空开额定电流,会引起空开跳闸; 2.不同空开实际载流能力受温度影响不同,当温度升高时空开载流能力降额,具体降额情况与空开本体相关。实际功耗不到1100W时可能存在跳闸风险; 3.空开选择与拉远距离、模块功率、电源线径等相关,拉远距离越大,电源线的压降损耗增加,模块端的输入电压下降,工作电流升高,也会导致前级空开跳闸; 4.模块前级空开计算要以模块最大功耗计算,不能以典型功耗计算。 注:RRU5516e模块典型功耗为1000瓦,考虑到温度导致的功耗上升,实际可能在1100瓦左右,电流为1100/36=30.6A左右

Global site tag (gtag.js) - Google Analytics