脏标记遍历之极致优化

本文以游戏中玩家属性同步为例,指出传统脏标记遍历方法存在性能瓶颈。介绍了之前的优化算法及其不足,提出利用gcc内置函数和位运算的新优化算法,能在k的时间判断出置脏属性。通过性能对比,新算法性能提升77倍,适用于所有脏标记相关功能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

欢迎关注作者git博客

1.背景介绍

  本文适用于所有脏标记遍历功能,提升性能几十倍,本文以游戏中玩家的属性同步作为例子进行介绍。

  MMORPG游戏中玩家有大量属性需要从服务器同步给客户端,例如名字、血量、战力、等级、速度等等。每当某个玩家的某个属性发生变化时,需要将玩家该属性的标志位置脏,在心跳或者其他需要发送的时机,调用同步属性函数,判断置脏的属性并同步给所有客户端。需要注意,不能每次在某个玩家的某个属性发生变化时,均同步给所有客户端,因为开销太大,一般在心跳里(固定时间间隔)同步。为了保证玩家属性在所有客户端及时更新,心跳间隔一般在500ms左右,如此频繁的调用以及同步属性函数内大量的脏标记判断导致同步属性函数成为性能瓶颈之一,大约占后台性能消耗的10%。

  假设玩家有n个属性,k个属性置脏,最直观的做法是遍历所有n个属性,逐个判断是否置脏,置脏的属性同步给客户端,需要判断n次;本文作者之前提过一种优化算法,根据属性置脏的频率,优先判断置脏频率高的属性,当判断出的脏属性数目等于k,则停止,该优化效率显著优于遍历n个属性,但也不是最优的算法;还有一种优化思路是用set记录置脏的属性,set插入时间复杂度为O(logn),置脏m个属性,set插入操作总时间复杂度为O(log(1*2*…*m)),再考虑某个属性频繁置脏的情况,比如第m个属性在同步之前置脏p次,则set操作时间复杂度为O(log(1*2…*m*p),虽然一般情况下m比较小,但不排除某些情况m很大,就会导致出现性能瓶颈,实测中发现采用set,o2编译,如图1.1所示插入50个脏属性耗时165639微秒,如图1.2所示置脏50个标记位耗时1522微秒,set效率低108倍,虽然同步的时间间隔内更新的属性数目很少,但某个属性更新的可能非常频繁,所以同步时间间隔内假设插入50个脏属性模拟很合理。显然,如果算法无论属性更新是否频繁,无论属性数目是多是少,都能在k的时间判断出哪些属性置脏,则该算法为最优算法。因为置脏的属性非常稀疏,k往往是个位数,所以最优算法可以将效率提升几十倍。


在这里插入图片描述
图1.1
在这里插入图片描述
图1.2

2.优化算法

  为引出本文的优化算法,本文首先介绍一个问题:计算一个整数的二进制表示中有多少个1。基本做法如图2.1所示。假设该整数n为10001100,则需要右移7次。但计算机里的数字本来就是用二进制存的,所以计算过程也都是二进制计算。利用一些位运算的特性,可以很容易计算1的个数。该例子中n - 1为10001011,n & (n - 1)为10001000,可以看到低3位都变成了0。多验证几个例子,可以看出结论,要消除整数n最低位的1,可以使用 n = n & (n-1)。从而计算n的二进制表示有多少个1的算法如图2.2所示。


在这里插入图片描述
图2.1
在这里插入图片描述
图2.2

  本文介绍一个gcc的内置函数,__builtin_ffs(uint32 n),返回n最后一个为1的位是从后向前的第几位,例如若n为10001100,则__builtin_ffs(n)为3,该函数对应的x86汇编代码,O0编译结果如图2.3所示,仅有四条汇编指令。该内置函数64位的版本为__builtin_ffsll(uint64 n)。


在这里插入图片描述
图2.3
  接下来正式介绍本文的优化算法,假设有64个属性,该64个属性用bit位表示是否置脏,共需64bit,将64bit转成uint64的n,每次用__builtin_ffsll找出最低位的1的位置,即可找出一个置脏的属性,然后用n&(n-1)消除最低位的1,当n为0时算法终止。假设64个属性第2个属性和第9个属性置脏,则该64bit编码为0…0100000010,首先用__builtin_ffsll找出低第2位的置脏属性,然后n=n&(n-1),即0…0100000000,再用__builtin_ffsll找出低第9位的置脏属性,然后n=n&(n-1)即为0,算法终止,可看出从64个属性里找出置脏的两个属性,只需要两次操作。需要注意,为尽可能提升性能,脏标记数目最好为64位整数倍,否则假如脏标记数目为120位,取出64位后,需要将剩下的63位拆分成32位、16位、8位,分三次取出,再计算脏标记位置,性能会降低。

3.性能对比

  num类型为uint64,记录所有脏标记,num低第2位、低第64位置脏,即num为100…010。图3.1所示为遍历所有属性,并逐个判断是否置脏,num & 1不为0时,表示第j个属性被置脏,图3.2所示为采用本文的优化算法,index为最低位的脏标记的位置。实测中图3.1耗时2011微秒,图3.2耗时26微秒,性能提升77倍。本文的优化算法不仅适用于属性同步,所有脏标记相关的功能均可采用,例如数据的判断置脏并落地存储。


在这里插入图片描述
图3.1
在这里插入图片描述
图3.2

<think>嗯,用户之前让我介绍了一个用Java实现快速排序的方法,现在他们要求再优化一下,写得更简单点。我需要先回顾一下之前的代码,看看哪里可以简化。 之前的快速排序代码有分三个方法:sortArray、quickSort、swap。用户可能觉得代码结构有点复杂,特别是对于刚学Java的人来说。优化的话,可能需要减少代码行数,或者用更简洁的逻辑。 首先,考虑是否可以将quickSort和swap合并到sortArray里,但这样可能影响可读性。或者,看看快速排序的实现能否简化,比如基准值的选择或者递归部分的处理。 用户提到“写简单点”,可能希望代码更简洁,容易理解。比如,使用更少的变量,或者减少代码嵌套。比如,基准值的选择,之前选的是中间元素,可能可以改成第一个元素,虽然这不是最优的,但代码更简短。 另外,swap方法可以内联到quickSort里,减少一个方法调用,这样代码行数减少,但可能影响复用性。不过对于简单的例子来说,可能没问题。 或者,是否可以考虑其他排序算法,比如冒泡排序,虽然效率低,但代码更简单。不过用户之前用了快速排序,可能还是希望保持较高效率的前提下简化代码。 再想想,快速排序的核心是分治,基准的选择和分区。或许可以简化分区过程,比如使用单边遍历或者更简洁的循环结构。例如,Lomuto分区方案比Hoare分区方案代码更简短,但可能效率稍低,不过代码更简单。 比如,Lomuto分区通常用一个索引来标记小于基准的边界,然后遍历数组交换元素。这可能更容易实现,代码更少。 那我可以尝试用Lomuto分区来重写快速排序,这样主方法可能更简洁。同时,将递归调用和分区合并到更少的代码行中。 另外,检查是否需要处理空数组,之前的代码已经处理了,可能可以保留,但缩短判断条件。或者将递归条件简化,比如去掉多余的判断。 再考虑代码注释和结构,用户可能希望去掉注释,或者减少方法数量。比如,把swap内联,去掉注释,使代码更紧凑。 总结可能的优化点: 1. 使用Lomuto分区方案,减少分区代码的复杂度。 2. 内联swap方法,减少方法数量。 3. 简化基准选择,比如选最后一个元素作为基准。 4. 减少递归调用中的条件判断。 5. 合并方法,将递归函数整合到主方法中,但可能影响可读性。 不过,合并方法可能不太现实,因为递归需要参数。所以可能还是保持两个方法,但简化内部实现。 比如,使用Lomuto分区的快速排序示例: public class SimpleSorter { public static void sort(int[] arr) { if (arr == null || arr.length < 2) return; sort(arr, 0, arr.length - 1); } private static void sort(int[] arr, int low, int high) { if (low < high) { int pivot = partition(arr, low, high); sort(arr, low, pivot - 1); sort(arr, pivot + 1, high); } } private static int partition(int[] arr, int low, int high) { int pivot = arr[high]; int i = low; for (int j = low; j < high; j++) { if (arr[j] < pivot) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; i++; } } int temp = arr[i]; arr[i] = arr[high]; arr[high] = temp; return i; } } 这个版本使用了Lomuto分区,代码相对简洁,但可能比之前的Hoare分区效率稍低。不过对于用户来说,代码更简短,而且方法结构更清晰。 但是用户要求更简单,可能需要更少的代码行数。例如,将partition内联到sort方法中,但这样可能反而更复杂。 或者,使用更简洁的交换方式,比如使用异或交换,但可能影响可读性,不推荐。 另外,检查是否需要处理数组为null的情况,原代码已经处理,可以保留。 所以,最终的简化版本可能是使用Lomuto分区,合并swap操作,减少代码行数,并保持结构清晰。可能还可以将sort方法中的递归部分更简洁地表达。 另一个考虑是,用户可能需要的是代码行数更少,而不是算法上的优化。所以可以尝试将方法合并,或者减少变量。 例如,将partition函数内联到sort中: public class SimpleSorter { public static void sort(int[] arr) { if (arr == null || arr.length < 2) return; sort(arr, 0, arr.length - 1); } private static void sort(int[] arr, int low, int high) { if (low >= high) return; int pivot = arr[high]; int i = low; for (int j = low; j < high; j++) { if (arr[j] < pivot) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; i++; } } arr[high] = arr[i]; arr[i] = pivot; sort(arr, low, i - 1); sort(arr, i + 1, high); } } 这样代码更简短,只有两个方法,swap内联,没有额外的partition方法。可能更符合用户的需求。 但需要测试是否正确。例如,基准选择最后一个元素,分区过程是否正确。这样写的话,代码行数减少,结构更简单。 原来的快速排序代码可能用了Hoare分区,而Lomuto分区的代码更简短,虽然交换次数可能更多,但代码更简单。 所以,用户可能希望看到这样的简化版本。同时,可以指出这种方法的优缺点,比如基准选择对性能的影响,但用户主要关注代码简洁性。 另外,还可以考虑使用更简短的变量名,比如将low改为l,high改为h,但这样可能降低可读性,不过用户可能接受。 总之,最终的简化版本应该减少方法数量,使用更简洁的分区策略,内联交换操作,并保持核心逻辑清晰。</think>以下是一个简化版的快速排序实现,代码更紧凑同时保持高效性: ```java public class SimpleSorter { public static void sort(int[] arr) { if (arr != null && arr.length > 1) { quickSort(arr, 0, arr.length-1); } } private static void quickSort(int[] arr, int l, int r) { if (l >= r) return; int pivot = arr[(l+r)/2]; // 取中间值作为基准 int i = l, j = r; while (i <= j) { while (arr[i] < pivot) i++; while (arr[j] > pivot) j--; if (i <= j) { int temp = arr[i]; arr[i++] = arr[j]; arr[j--] = temp; } } quickSort(arr, l, j); quickSort(arr, i, r); } } ``` ### 核心优化点 1. **代码精简**:从原有 3 个方法简化为 2 个方法 2. **变量简化**:使用单字母参数名(`l`=左边界,`r`=右边界) 3. **就地交换**:直接在循环内完成元素交换,省去`swap`方法 4. **基准优化**:保持中间值基准选择(避免最坏情况) 5. **边界处理**:保持空数组和越界检查 ### 使用示例 ```java int[] data = {5, 2, 9, 1, 5}; SimpleSorter.sort(data); System.out.println(Arrays.toString(data)); // 输出 [1, 2, 5, 5, 9] ``` ### 代码对比优势 | 原版代码 | 优化版代码 | |---------|----------| | 35 行代码 | 19 行代码 | | 3 个方法 | 2 个方法 | | 详细注释 | 自解释代码 | | 完整参数名 | 简洁参数名 | 如果需要更极致的简化(但会降低可读性),可以使用以下单方法实现: ```java public static void ultraSort(int[] arr, int l, int r) { if (l >= r) return; int i = l, j = r, p = arr[(l+r)/2]; while (i <= j) { while (arr[i] < p) i++; while (arr[j] > p) j--; if (i <= j) { int t = arr[i]; arr[i++] = arr[j]; arr[j--] = t; } } ultraSort(arr, l, j); ultraSort(arr, i, r); } // 调用方式 ultraSort(yourArray, 0, yourArray.length-1); ``` 建议实际开发中优先使用标准库`Arrays.sort()`,但若需要自实现排序,这个简化版已具备生产环境可用性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值