Why the “volatile” type class should not be used

本文探讨了在C编程中volatile类型修饰符的常见误解及其在内核代码中的不恰当使用。volatile主要用于抑制编译器优化,但在保护共享数据结构免受并发访问时,应使用锁、互斥量或内存屏障等机制。直接使用volatile可能导致代码效率降低或引入错误。

Why the “volatile” type class should not be used


C programmers have often taken volatile to mean that the variable could be changed outside of the current thread of execution; as a result, they are sometimes tempted to use it in kernel code when shared data structures are being used. In other words, they have been known to treat volatile types as a sort of easy atomic variable, which they are not. The use of volatile in kernel code is almost never correct; this document describes why.

The key point to understand with regard to volatile is that its purpose is to suppress optimization, which is almost never what one really wants to do. In the kernel, one must protect shared data structures against unwanted concurrent access, which is very much a different task. The process of protecting against unwanted concurrency will also avoid almost all optimization-related problems in a more efficient way.

Like volatile, the kernel primitives which make concurrent access to data safe (spinlocks, mutexes, memory barriers, etc.) are designed to prevent unwanted optimization. If they are being used properly, there will be no need to use volatile as well. If volatile is still necessary, there is almost certainly a bug in the code somewhere. In properly-written kernel code, volatile can only serve to slow things down.

Consider a typical block of kernel code:

spin_lock(&the_lock);
do_something_on(&shared_data);
do_something_else_with(&shared_data);
spin_unlock(&the_lock);

If all the code follows the locking rules, the value of shared_data cannot change unexpectedly while the_lock is held. Any other code which might want to play with that data will be waiting on the lock. The spinlock primitives act as memory barriers - they are explicitly written to do so - meaning that data accesses will not be optimized across them. So the compiler might think it knows what will be in shared_data, but the spin_lock() call, since it acts as a memory barrier, will force it to forget anything it knows. There will be no optimization problems with accesses to that data.

If shared_data were declared volatile, the locking would still be necessary. But the compiler would also be prevented from optimizing access to shared_data within the critical section, when we know that nobody else can be working with it. While the lock is held, shared_data is not volatile. When dealing with shared data, proper locking makes volatile unnecessary - and potentially harmful.

The volatile storage class was originally meant for memory-mapped I/O registers. Within the kernel, register accesses, too, should be protected by locks, but one also does not want the compiler “optimizing” register accesses within a critical section. But, within the kernel, I/O memory accesses are always done through accessor functions; accessing I/O memory directly through pointers is frowned upon and does not work on all architectures. Those accessors are written to prevent unwanted optimization, so, once again, volatile is unnecessary.

Another situation where one might be tempted to use volatile is when the processor is busy-waiting on the value of a variable. The right way to perform a busy wait is:

while (my_variable != what_i_want)
    cpu_relax();

The cpu_relax() call can lower CPU power consumption or yield to a hyperthreaded twin processor; it also happens to serve as a compiler barrier, so, once again, volatile is unnecessary. Of course, busy- waiting is generally an anti-social act to begin with.

There are still a few rare situations where volatile makes sense in the kernel:

The above-mentioned accessor functions might use volatile on architectures where direct I/O memory access does work. Essentially, each accessor call becomes a little critical section on its own and ensures that the access happens as expected by the programmer.
Inline assembly code which changes memory, but which has no other visible side effects, risks being deleted by GCC. Adding the volatile keyword to asm statements will prevent this removal.
The jiffies variable is special in that it can have a different value every time it is referenced, but it can be read without any special locking. So jiffies can be volatile, but the addition of other variables of this type is strongly frowned upon. Jiffies is considered to be a “stupid legacy” issue (Linus’s words) in this regard; fixing it would be more trouble than it is worth.

Pointers to data structures in coherent memory which might be modified by I/O devices can, sometimes, legitimately be volatile. A ring buffer used by a network adapter, where that adapter changes pointers to indicate which descriptors have been processed, is an example of this type of situation.

For most code, none of the above justifications for volatile apply. As a result, the use of volatile is likely to be seen as a bug and will bring additional scrutiny to the code. Developers who are tempted to use volatile should take a step back and think about what they are truly trying to accomplish.

该异常信息 `java.lang.Throwable: the expensive method should not be called inside the highlighting pass` 通常出现在使用 IntelliJ IDEA 或基于其平台的 IDE(如 Android Studio)进行插件开发或代码分析时。该提示表明在代码高亮处理阶段调用了性能开销较大的方法,这可能导致 IDE 在渲染代码时出现卡顿或响应迟缓的情况。 ### 异常原因 在 IntelliJ 平台中,代码高亮处理是一个频繁触发的过程,用于在编辑器中动态地为代码添加语法高亮、错误提示、快速修复等功能。如果在此阶段执行了耗时操作,例如文件 I/O、网络请求、复杂的计算逻辑等,会导致编辑器性能下降,影响用户体验。 平台会通过抛出 `java.lang.Throwable` 的特定子类或直接中断操作来提示开发者避免在该阶段执行昂贵操作[^1]。 ### 解决方法 1. **延迟执行昂贵操作** 将耗时操作从高亮处理流程中移除,改用异步方式或延迟加载机制。例如可以使用 `com.intellij.util.ui.update.UiUpdater` 或 `com.intellij.openapi.application.ApplicationManager` 提交到后台线程执行: ```java ApplicationManager.getApplication().executeOnPooledThread(() -> { // 执行昂贵操作 }); ``` 2. **缓存计算结果** 如果昂贵方法的结果可以在高亮处理之外的阶段预计算并缓存,则应在高亮阶段仅使用缓存值,避免重复计算。 3. **使用轻量级替代方法** 替换原本的昂贵方法为轻量级版本,例如将完整的 AST 遍历替换为局部节点检查,或将完整的类型解析替换为快速符号匹配。 4. **利用 PSI 性能优化接口** 在 IntelliJ 平台中,可以使用 `PsiElementVisitor` 的轻量级实现,或使用 `PsiTreeUtil` 提供的高效查找方法来替代递归遍历整个 PSI 树。 5. **使用 HighlightVisitor 的 before/after 模式** 如果自定义的高亮逻辑必须依赖某些昂贵数据,可以考虑在 `HighlightVisitor` 的 `analyze()` 方法中预加载所需数据,并在 `visit()` 方法中仅使用已加载的数据进行高亮处理。 ### 示例代码:异步加载数据以避免阻塞高亮阶段 ```java public class MyHighlightVisitor extends PsiElementVisitor { private volatile MyData cachedData; public void analyze(PsiFile file) { // 异步加载数据 ApplicationManager.getApplication().executeOnPooledThread(() -> { cachedData = computeExpensiveData(file); }); } @Override public void visitElement(PsiElement element) { if (cachedData != null) { // 使用缓存数据进行高亮处理 if (cachedData.contains(element)) { // 应用高亮 } } } private MyData computeExpensiveData(PsiFile file) { // 实现昂贵计算逻辑 } } ``` ### 调试建议 - 使用 IntelliJ 平台提供的性能分析工具(如 CPU Profiler)定位具体是哪个方法导致了性能瓶颈。 - 启用 `idea.is.internal.mode` 系统属性以获取更详细的日志输出,帮助定位高亮阶段的性能问题。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值