![]() |
|
[教程] 代码优化 #1 - 打印版本 +- samp | open.mp 联机社区论坛 (https://open-mp.cn) +-- 板块: SA-MP (https://open-mp.cn/forumdisplay.php?fid=12) +--- 板块: 教程 (https://open-mp.cn/forumdisplay.php?fid=17) +--- 主题: [教程] 代码优化 #1 (/showthread.php?tid=4) |
[教程] 代码优化 #1 - XiaoNiao - 02-28-2026 [u][教程] 代码优化 #1[/u] 优化技巧总数:13 | 原文作者: Yashas 本文包含以下技巧:
其中一些技巧会带来显著改进,而有些则不然。你可以忽略一些次要优化,并优先编写可读性强的代码。 优化技巧 1: 数组比普通变量慢 以下代码效率低下: 代码: new Float:pos[3];这是上述代码的汇编版本: 代码: zero.pri现在,这是等效的更高效代码: 代码: new Float:x, Float:y, Float:z;这是汇编版本: 代码: push.c 0 //Making room for the variables on the stack当你想访问数组元素时,编译器使用以下算法: 第一个元素的地址 + 4*索引 = Array[Index] 存储的位置(此公式仅适用于一维数组) 计算数组元素的地址后,即可检索元素中存储的数据。 这并不意味着你不能使用数组。你必须明智地使用数组。当可以使用普通变量简单实现时,不要无缘无故创建数组。 在我看来,使用 x、y、z 实际上比使用数组 pos[3] 更具可读性。 速度测试: 数组(10 次赋值):2444,2448,2473 非数组(10 次赋值):972,975,963 速度测试代码:http://pastebin.com/aMkNtaC2 非数组版本比数组版本快 2.5 倍。 优化技巧 2: 当提前知道函数名时,不要使用 CallLocalFunction 和 funcidx 你知道 CallLocalFunction 和 funcidx 是慢函数吗?它们非常慢,因为它们需要在所有公共函数列表中检查你作为参数传递的函数名。这意味着大量内部 strcmp 操作。 代码: if(funcidx("OnPlayerEatBanana") == -1)你实际上不需要那行代码。你可以用"0"指令实现相同功能。如果你已经知道函数名,只需使用预处理器指令来检查函数是否存在。 代码: #if defined OnPlayerEatBanana代码: if(CallLocalFunction("OnPlayerEatBanana","ii",playerid,bananaid"))你可以这样写: 代码: #if defined OnPlayerEatBanana速度测试:(1 个零参数的公共函数) 直接调用:204,226,218 CallLocalFunction:1112,1097,1001 请注意,这是 CallLocalFunction 的最佳情况。在现实中,由于有许多公共函数,CallLocalFunction 会慢得多。 优化技巧 3: 原生函数比 Pawn 代码快得多 当有原生函数可以实现时(或使用原生函数组合),避免创建自己的函数。 原生函数快得多的原因是,原生函数直接由你的计算机执行,而所有 Pawn 代码都在虚拟机中执行。对于每个 Pawn 指令,AMX 机器(虚拟计算机)必须解码指令、获取操作数,然后执行指令。解码和获取操作数会消耗一些 CPU。 代码: stock strcpy(dest[], src[], sz=sizeof(dest))速度测试: 基于循环的 strcpy 与原生 strcat 这里是两个等效的 strcpy 函数。 http://pastebin.com/Y7RJ21tw 原生:697,700,718,705 非原生:5484,5422,5507,5562 优化技巧 4: 循环中的条件 我不知道我已经告诉过人们多少次了,但仍有一些人没有进行这个简单优化。 代码 1: 代码: for(new i = 0;i <= GetPlayerPoolSize();i++) {}代码 2: 代码: for(new i = 0,j = GetPlayerPoolSize();i <= j;i++) {}在第一个代码中,每次迭代都会调用 GetPlayerPoolSize。在我们的时间框架中,GetPlayerPoolSize 每次调用都返回常量值。那么为什么每次迭代都调用 GetPlayerPoolSize? 第二个代码避免了这一点。它创建一个局部变量来存储 GetPlayerPoolSize 返回的值,并在条件中使用它。因此只调用函数一次,避免函数开销。 速度测试: 优化后:1102,1080,1069,1091 未优化:2374,2359,2429,2364 测试代码:http://pastebin.com/SLZDGRG4 虽然在上述情况下改进可能相对于循环内部代码微不足道,但有时你会使用更慢的函数。 代码: for(new i = 0; i < CallRemoteFunction("GetPlayersInTeam", "i", TEAM_ID); i++) 优化技巧 5: 将多个变量赋值为相同值 & 使用 memset 代码 1: 代码: x = abc;代码 2: 代码: x = 你认为哪个代码更快? 代码 1: 代码: load.pri c ;Get abc代码 2: 代码: load.pri c ;Get abc看到区别了吗?第一个代码有额外无用的指令,它反复获取 abc,而它已经存在;第二个版本只获取 abc 一次,并设置 x、y、z。 显而易见,代码 2 更快,但这可能无关紧要。 当你有大数组需要设置为零、一或其他值时,使用 memset。 速度测试: 使用 memset 将 100 个元素的三维数组的所有元素设置为零:363,367,372 使用 for 循环将 100 个元素的三维数组的元素设置为零:6662,6642,6687 优化技巧 6: 延迟声明局部变量 我见过一些脚本将所有局部变量放在函数顶部,尽管有些变量有时才需要。示例应该能说明问题。 不良代码: 代码: public OnPlayerDoSomething(playerid)良好代码: 代码: public OnPlayerDoSomething(playerid)如果你阅读了前面的提示,你现在应该知道,当创建局部变量时,编译器首先在栈中为其创建空间,然后将其初始化为零。 所以,如果你不确定是否会使用局部变量,就不要简单地创建它们。第二个代码仅在需要时创建局部变量,而第一个代码即使可能不使用也会创建它们。 这对少数变量的性能没有显著影响,但它提高了代码的可读性。 优化技巧 7: 简化并改写数学表达式以避免昂贵操作 我在编写程序时总是保持笔和纸在桌子上。我在纸上写方程,进行一些移位和更改,得到更简单的方程。 这是一个经典示例,它将提升此代码段的性能: 代码: new Float:x,Float:y,Float:z;代码: new Float:x,Float:y,Float:z;你注意到变化了吗? 我在 if 语句的条件两边平方,消除了慢函数 'floatsqrt'。 另一个示例: 代码: for(new i = 0, j = GetTickCount(); i < 10; i++)代码: for(new i = 0, j = GetTickCount() - MAX_TIME_ALLOWED; i < 10; i++)哇,我从条件中移除了 MAX_TIME_ALLOWED。现在减法只执行一次,而第一个代码中每次都执行。即使这个改进无关紧要,除非你有消耗大量 CPU 的操作。 优化技巧 8: memcpy、strfind 等也适用于数组 毕竟字符串和数组是一回事。唯一的区别是字符串以空字符终止,而普通数组没有。 代码: new DefaultPlayerArray[100] = {1,2,3,4,5,6,7,8,9,10};这是另一个等效代码: 代码: memcpy(PlayerArray[playerid], DefaultPlayerArray, 0, sizeof(DefaultPlayerArray)*4, sizeof(PlayerArray[]));我对两个代码进行了基准测试,结果如下: 循环版本: 4286ms 4309ms 4410ms memcpy 版本: 60ms 62ms 60ms 同样,你可以使用 strfind、strmid 和许多其他字符串函数处理数组。唯一的问题是,当字符串函数在数组中找到元素 '0' 时,函数会终止,因为值 0 表示 '\0',即空字符。 优化技巧 9: 使用 CallRemoteFunction 真的值得吗? 首先,我想说 CallRemoteFunction 非常慢,必须尽可能避免。CallRemoteFunction 通常用于在其他脚本中有反作弊时更新玩家变量。 你有没有想过在每个脚本中都有反作弊?我实际上在游戏模式中有一个反作弊,确保修改的数据不更新到数据库中;在管理过滤脚本中另一个反作弊处理对作弊的行动(独立工作)。 为什么有两个反作弊?我们有两个选择,要么创建两个反作弊,要么使用 CallRemoteFunction 更新玩家变量。 有时有两个独立的反作弊更快,事实上,有些反作弊检查只需 CallRemoteFunction 调用更新函数时间的四分之一。 如果你在每个脚本中计算一些玩家变量也没关系。它比在一个脚本中更新并使用 CallRemoteFunction 访问要好得多。 优化技巧 10: 多次访问数组元素 让我们用一个示例来理解我们正在讨论的内容: 代码: new val = value[x][y][z];代码: for(new i = 50; i != -1; --i) Arr[i] = value[x][y][z];你认为哪个更快? 如果你仔细阅读了技巧 #2,第一个更快。你知道从数组索引计算正确地址需要一些时间。在第二个代码中,每次将值复制到 Arr 时都会进行地址计算,而在第一个情况下,我们只计算地址一次。 takeaway 信息是,如果你将多次访问数组元素,则在局部变量中创建数组元素的临时副本,并使用局部变量。 速度测试: 代码 1:2280,2330,2350 代码 2:8008,8183,8147 优化技巧 11: 不要在表达式中混合浮点数和整数(由 Mauzen 贡献) 也许这个太简单了,但我至少想添加它,因为我经常看到人们犯这个"错误"。 永远不要混用浮点数和整数(即使没有标签不匹配警告)。始终在单个语句中使用相同的数据类型。 例如: 代码: new Float:result = 2.0 + 1;优化技巧 12: 不必要地使用 Streamer 每个人都习惯使用 Streamer,即使只需要 10 或 20 个地图图标、50 个对象等。 你知道什么是 Streamer 吗?Streamer 是一个插件/包含文件,允许你绕过 SAMP 限制。SAMP 允许最多 1000 个对象,你不能超过这个数量。 Streamer 通过在玩家进入对象绘制距离时创建对象,并在没有玩家靠近时销毁对象来绕过限制。所以基本上,Streamer 在需要时创建对象,并在不需要时销毁它们。这样它允许你超过 SAMP 限制。 当你使用 Streamer 函数,如 CreateDynamicObject 时,Streamer 并不真正创建对象。它将对象信息(X,Y,Z,RotX,RotY,RotZ....)添加到对象数据库中。经过一定数量的服务器 tick/周期后,它遍历数据库中的所有对象,检查是否有玩家靠近对象,并在需要时创建它。 你可以在 这里 看到 Streamer 将对象信息添加到数据库。 玩家更新从 这里 开始。 这是负责更新对象的函数。 当你少于 1000 个对象时,使用 Streamer 有意义吗? 你真的需要 Streamer 吗? 不! 如果你确定不会超过 SAMP 限制,那么就不需要使用 Streamer。 这带来了一个新问题,假设你的现有版本有 500 个对象,但你要更新脚本需要 1500 个对象。那么现在你需要将所有 SAMP 对象原生转换为 Streamer 原生吗? 如果你的初始版本写得聪明,就不需要。 这是我做的: 代码: #define CreateDynamicObject CreateObject现在你可以在代码中使用 CreateDynamicObject,即使你没有 Streamer。 当你知道需要 Streamer 时,只需移除定义并包含 Streamer。 一个更聪明的方法是为位于热门区域的对象使用 CreateObject,例如生成点,你可以假设玩家几乎总是存在于该位置。对于位于偏远位置且玩家很少访问的对象,你绝对应该使用 CreateDynamicObject,因为这些对象不需要一直创建,而热门对象无论如何都会存在(即使使用 Streamer,所以为这样的对象使用 Streamer 不值得成本)。 你必须为许多对象这样做才能看到合理的改进,因为 Streamer 是插件,因此它比 Pawn 代码快得多。 同样,你可以为其他原生这样做。 优化技巧 13: 函数的良好与不良使用(优化 2D 数组操作代码) 一个常见神话是许多人相信"函数调用非常昂贵",这不是真的。事实上,原始函数调用(空)比解引用 2D 数组快很多倍。 代码: native SLE_algo_foreach_list_init(list:listid, &val);如果你仔细看,它只是调用函数来获取值,这比解引用数组快。要完全消除疑虑,该函数定义在插件中,否则由于明显原因它不会更快,因为里面确实使用了数组,但那是插件内部。 这个例子只是为了说明函数调用相对于你编写的其他代码并不那么昂贵。这意味着当必要时,你应该为执行特定任务的大块代码创建函数,特别是如果它提高了代码的可读性。 然而,函数的误用可能代价高昂,特别是在循环中,这在前面已经讨论过。 这里是一个使用 1D/2D 数组会更好的情况。 引用:引用:最初由 Vince 发表于 另一个神话是"创建函数总是更慢",这在处理多维数组时完全不对。如果做得正确,创建函数实际上可以显著提高性能。 代码: for(new y = 0; y < 100; y++)比以下慢得多: 代码: stock DoSomething(arr[])原因在于汇编代码的根源。快速查看 Pawn 中数组如何解引用就能解释。 这是解引用 2D 数组涉及的代码量: 代码: #emit CONST.alt arr //Load the address of the array与解引用 1D 数组相比: 代码: #emit CONST.alt array_address这是数组传递的方式: 代码: //Pushing the address of the global string这清楚地解释了为什么有效。当你推送数组时,你推送数组的地址,因此在函数调用中你收到一个 1D 数组。在这种情况下,2D 数组解引用的第一部分代码本质上在每次迭代中被跳过,这使它快得多。 遗憾的是,Pawn 不提供指针。 |