全网资源采集网站搭建

电脑端+手机端+微信端=数据同步管理

免费咨询热线:13114099110

当前位置: 主页 > 新闻资讯

网站代码优化-(如何优化网站seo优化效果才好)

发布时间:2023-01-18 10:08   浏览次数:次   作者:派坤优化

网站代码优化-(如何优化网站seo优化效果才好)(图1)

来源|写在前面 作为一名前端开发者,我一直难以理解学习计算基础知识的重要性,因为我很难想象这些知识将如何应用到前端开发的工作中. 但是在csapp优化程序性能那一节,彻底改变了我的想法。 作者描述了如何优化正确且编写良好的 C 语言程序的性能。 对于一段功能相同的代码,优化后与优化前有巨大的差异,速度提升了数十倍。 然而,让我感到困惑的是,对于js和c这两种语言层面差异如此之大的语言,这样的优化是否有同样的效果? 经过实践,答案是肯定的。 所以计算机基础知识不是空中楼阁,它是真正有用的,可以让你写出更好的程序。 为什么大厂都喜欢考察基础,因为这些基础确实在某些方面反映了一个人编程的水平,而对于计算机专业的学生来说,这恰恰是可以拉开与他人差距的地方。介绍先举个例子C语言的。 下面代码的作用是将字符串中的大写字母转为小写字母

void lower(char *s) { size_t i; for (i = 0; i < strlen(s); i++) { if (s[i] >= 'A' && s[i] <= 'Z') s[i] -= ('A' - 'a'); }}

你能看出这段代码的问题出在哪里吗? 如果你足够敏感,结合字幕的提示,你会发现问题出在for循环()函数的判断条件上。 首先指出这段代码的正确性无疑是功能性的,但性能上堪忧。 这里给不懂函数的小伙伴提示一下:函数是通过遍历字符串获取它的长度的,所以每次for循环都会遍历一次字符串s,所以这段代码的时间复杂度为o(n^2),但实际上我们只是修改s的每一个值,而不是删除或添加某个值,我们不需要每次都计算s的长度。 您可能想知道编译器是否无法识别这种模式并针对它进行优化? 答案是不。 简单的说,就是因为编译器无法判断是否有副作用导致判断条件发生变化。 使用内存别名也是有原因的。 我不会在这里详细解释它们。 如果你有兴趣,你可以看看这本书。 当然这段代码的优化很简单,就是通过一个临时变量在循环前计算调用一次,然后将该变量作为判断条件。 值得注意的是,仅仅这么简单的优化,时间复杂度就从o(n^2)变成了o(n),这是一个巨大的提升。 有经验的程序员可能会认为这段代码的问题是C语言特有的,因为大多数高级语言中字符串的长度都是由语言本身保持为一个常量,不需要遍历就可以得到。 事实上,这个例子只是一个提醒,小的改变可能会导致巨大的性能下降或提升,它的性能差距。 在for循环判断中调用该函数获取数组的长度。 相比之下,这个函数的时间复杂度是O(1)。 可能有人会问? 为什么脱裤子放屁,为什么不用。 直接属性,这里有以下考虑: 至于为什么要用函数获取数组元素,因为js数组没有越界检查(越界访问不会报错),程序可能运行了很长时间错误才显现出来。 所以目的是提供越界检查,越界时抛出错误。 版本 1:

// 本代码可直接运行
function getFromArray(arr, index) { // 提供越界检查的数组getter if (index < 0 || index >= arr.length) throw new Error('OUT OF INDEX' + index); return arr[index]}
function getLengthFromArray(arr) { // 得到数组长度 if (arr == null) throw new Error(arr + 'is not an Array.') return arr.length; // 即使是常数级的函数调用,依旧产生开销}
void function() { console.log('start');
const numberOfElement = 9999999; const arr = Array(numberOfElement).fill(2);
(function combine1() { console.time('combine1');
let sum = 0; for (let i = 0; i < getLengthFromArray(arr); i++) { // 每次循环都要重新计算长度 sum += getFromArray(arr, i); // 使用提供越界检查getter得到数组元素 } console.log('sum', sum);
console.timeEnd('combine1'); }()); console.log('end');}();

消除循环的低效率函数将在每个for循环中被调用作为循环的测试条件。 根据我们函数的作用,我们只访问数组中的每个元素,不会修改数组的长度,所以很多 的无效计算(调用),所以需要优化。

(function combine2() {// 可直接执行 console.time('combine2'); let sum = 0; const len = getLengthFromArray(arr); // 消除每次循环对数组长度的计算 for (let i = 0; i < len; i++) { sum += getFromArray(arr, i); } console.log('sum', sum); console.timeEnd('combine2');}());

通过我个人电脑的测试,去掉for循环判断条件中无用的计算后,确实比对比快了10ms左右,你可能觉得这个时间不算什么。 BUT:无论此优化带来的改进是否巨大,它都是需要消除的低效源,否则可能成为进一步优化的瓶颈。 减少冗余函数调用 通过分析函数,我们可以发现,如果正确设置循环的终止条件,函数提供的越界检查似乎是不必要的。 每次访问都会做一次判断,这是不必要的,而且每次函数调用也是一种开销。

(function combine3() { /** * 现在消除了每次循环对数组长度的计算 * 并且确定访问不会越界,不需要越界检查,直接访问数组 */ console.time('combine3'); let sum = 0; const len = getLengthFromArray(arr); for (let i = 0; i < len; i++) { sum += arr[i]; // 直接访问 } console.log('sum', sum); console.timeEnd('combine3');}());

经过实践,对比性能几乎翻了一番,所以从性能上来说,这种优化显然是需要的。 但是有一点值得争论的是,如果你把arr当作别人提供的数据结构,作为这个数据的结构,我们作为使用者网站代码优化,不应该对arr的底层实现有任何假设,我们不知道它是什么 is 数组仍然是一个链表,因此违反了黑盒原则,提高了程序的耦合度。 所以在优化程序的时候,你可能需要在高性能和高耦合或者低性能和低内聚之间进行权衡选择,以消除不必要的内存访问。 为了理解这个优化点,首先要明白: 弄清楚了这几点之后,让我们来看一段负优化后的代码:

(function combine4() { let sum = [0]; // 负优化在这里,sum现在是个指针了 console.time('combine4'); const len = getLengthFromArray(arr); for (let i = 0; i < len; i++) { sum[0] += arr[i]; // 三次内存访问 } console.log('sum', sum[0]); console.timeEnd('combine4');}());

唯一不同的是sum变成了引用类型的变量,指的是一个存放在内存中的长度为1的数组,此时我们分析sum[0] += arr[i]需要访问多少次内存:为了得出更有说服力的结论,这里提供另一个优化版本进行对比。

(function combine4_anothor() { let sum = [0]; let tmp = 0; // 通过设计临时变量,减少内存访问次数 console.time('combine4_anothor'); const len = getLengthFromArray(arr); for (let i = 0; i < len; i++) { tmp += arr[i]; // 一次内存访问 } sum[0] = tmp; console.log('sum', sum[0]); console.timeEnd('combine4_anothor');}());

下面我们来分析一下每个for循环的内存访问次数。 只有一次读取arr[i]的操作,相比原来减少了两倍的内存访问。 与每次循环都写入内存相比,选择在最后写入内存。 其实这个运算性能的提升也是巨大的。 这里我给出我电脑的结果: 这部分的核心是:通过设置临时变量,复用变量,减少不必要的内存访问。 在本节中,您可能会感到困惑:为什么内存速度这么慢? 寄存器和内存是什么关系? 为什么局部变量存储在寄存器中? 你是怎么做到的? 同样,这些问题在《CSAPP》中都有解答。使用loop

(function combine5() { /** * 利用循环展开 */ let sum = 0; console.time('combine5'); const len = getLengthFromArray(arr); const limit = len - 1; //防止越界 let i; for (i = 0; i < limit; i += 2) { // 步长为2的展开 sum += arr[i]; // 直接访问 sum += arr[i + 1]; } for (; i < len; i++) { //处理剩余未访问的元素 sum += arr[i]; } console.log('sum', sum); console.timeEnd('combine5');}());

循环展开如何提高这段代码的性能? 原因是它消除了调用 for 循环的部分开销。 在这个具体的例子中,它减少了大约一半的for循环次数,所以它也减少了一半的判断次数,所以性能会有所提高。 然而,令人惊讶的是:但是我在机器上练习的时候网站代码优化,当我使用步长为2的扩展时,并没有比跑步快,反而慢了一点点,但是当我逐渐增加数量的时候循环展开的步骤,时间越来越近,最后大致相同。 为什么是这样? 首先,正如刚才所说,循环性能提升的原因是减少了for循环中的判断次数。 当我们使用步长为 1 的循环时,现代编译器可以识别这种常见的循环模式并直接执行类似的循环展开。 ,所以当我们增加循环展开的步长时,我们是在手动进行优化。 但值得注意的是,对于这种简单的代码,编译器是可以识别的,但并不一定复杂,所以掌握这种技术是很有必要的,而这种通过loop 优化性能的来源是值得思考的。 再分析性能瓶颈 下面我们来分析一下性能限制,或者说这个函数的运行时间主要取决于什么因素? 我们可以通过循环展开来减少循环次数,从而节省每次循环后比较的开销,但是sum += arr[i]的开销是省不了的。 无论我们怎么优化,至少要访问arr数组次数,而计算机对于sum + = arr[i]的执行必须是顺序的,不管是否使用循环展开,因为每次计算的值sum 取决于前一个 sum 的计算值。 因此,函数的主要运行时间来自sum += arr[i]。 如果arr数组的长度是100,计算机每次执行sum += arr[i]需要1秒,所以不管怎么优化,每次执行至少要100s。 在这里,我们想介绍一下现代 CPU 的一些知识。 大家都以为代码(指令)是按顺序执行的,显示出来的效果确实是一样的,但在底层,CPU其实是乱序执行代码的。 通过分析和重新排序指令,CPU 可以并发甚至并行执行代码(通过利用多核)。 为什么这是正确的?考虑以下代码

let a = 1 + 1;let b = 2 + 2;let c = a + b;let d = c + c;

a和b可以并行计算,因为它们不相互依赖,而c必须等到a和b执行完才能运行,d必须等到c执行完才能执行,所以c和d不能并行执行,只能顺序执行。现在我们来回顾一下限制性能的原因:1)是不可避免的,但我们是否有机会改进2),考虑以下代码来提高代码并行性

(function combine5_another() { /** * 提高并行性 */ console.time('combine5_another'); let sum = 0; let tmp1 = 0; const len = getLengthFromArray(arr); const limit = len - 1; //防止循环展开后越界 let i; for (i = 0; i < limit; i += 2) { sum += arr[i]; // 直接访问 tmp1 += arr[i + 1]; } for (; i < len; i++) { //处理剩余未访问的元素 sum += arr[i]; } sum += tmp2; console.log('sum', sum); console.timeEnd('combine5_another');}());

通过设置一个临时变量tmp1来计算数组arr[2n - 1]的累加和,sum计算arr[2n - 2]的累加和,这样可以让cpu并行计算tmp1和sum,因为计算tmp1 不依赖于 sum,反之亦然。 最后,循环结束后,合并结果。 理论上,100s的执行时间减少到50s。 然而,在我的实践中,并行优化并没有带来性能的提升,甚至有所下降。 可能的原因有两个,一是干扰,二是很重要。 先说干扰。 这是因为:由于在并行计算中使用循环展开,编译器无法识别其循环模式并对其进行优化。 另一个重要的原因是循环展开和提高并行度这两个优化依赖于底层硬件。 以后者为例,CPU之所以能够进行并行计算,是因为底层有冗余的计算功能单元,而单元的数量限制了同时并行计算的数量**,对于同样的优化代码,运行在不同的cpu,性能提升不一定,甚至可能会降低**。 所以这两个优化需要具体机器具体分析,但原则上是通用的。 同样,《CSAPP》详细解释了这两个优化背后的原理和原因。 有兴趣的可以看书。 最后给出未优化和优化版本的对比: 大约70%的性能提升 利用程序的局部性 经常使用的数据存储在内存和CPU之间的缓存中,也就是当程序请求一段数据时,操作系统会先在缓存中寻找,然后在内存中寻找(虽然内存的传输速度比硬盘快很多,但是相对于CPU的处理速度,还是很慢的,所以在CPU和CPU之间加了一层缓存),然后去硬盘上找,这样就可以一层一层往下找了。 找到了就直接返回,每一关的搜索速度越来越慢。 其次,通俗地说,程序的局部性就是:刚刚访问的数据或执行的代码本身或其相邻的数据(代码)很可能(再次)被访问(执行)。 又分为: 例如:下面的代码对一个10*10的矩阵进行求和。

const matrix = getMatrix(10, 10);let sum = 0;for (let row = 0; row < matrix.length; row++) { for (let col = 0; col < matrix[0].length; col++) { sum += matrix[row][col]; }}

对于变量和,它很好地反映了时间局部性。 第一次访问后,以后的每个周期都会再次访问,所以操作系统在第一次访问时将其加入缓存后,每次都求和。 访问会直接访问缓存而不是内存,从而减少每次访问内存的时间消耗。 对 [row][col] 的访问反映了空间局部性。 为了理解这一点,首先要理解二维数组是如何存储在内存中的。 二维数组在内存中以行优先的方式存储。 例如对于一个10*10的二维数组arr,假设它的起始内存地址为0,那么每个元素对应的地址位:即如果我们先行后列的方式遍历(即上面代码的方式),那么我们遍历的内存地址顺序是线性连续递增的顺序:内存地址0,1,2,...,50,51,52,...,90,91,92, ..., 99 它反映了一种空间局部性。 当我们访问arr[0][0](内存地址0)时,操作系统根据空间局部性的原则假设我们很可能访问arr[0][1],所以将其加载到缓存中,而当我们真正访问它的时候,不需要等待取内存,而是直接从缓存中获取。 为了比较,我们介绍了一个不好的例子。

let sum = 0;for (let col = 0; col < matrix[0].length; col++) { for (let row = 0; row < matrix.length; row++) { sum += matrix[row][col]; }}

这段代码和上面这段代码的功能完全一样,唯一不同的是它的遍历方式改为先list再row。 为了给大家一个直观的理解,此时遍历内存的地址顺序为:0,10,20,...,80,90,1,11,21,...,81,91,2, 12, 22, .... 和刚才的连续增量遍历相比,这个遍历跳动了一点。 这样做的问题是不能在局部性的前提下利用系统提供的缓存机制。 当你访问arr[0][0]时,系统会将arr[0][1]加入到缓存中,然后你直接跳转到arr[1][0],如果没有命中缓存,则需要等待用于获取内存。 当此类操作大量累积时,将导致程序性能相当低下。 这是矩阵求和的松散但直观的示例代码结果:

startsum 99980001良好的局部性: 159.879mssum 99980001坏的局部性: 3420.815msend

// 本代码可直接复制到浏览器运行void function() { console.log('start'); const size = 9999; const matrix = Array(size); for (let i = 0; i < matrix.length; i++) { matrix[i] = Array(size).fill(1); }
(function goodVersion() { console.time('良好的局部性'); let sum = 0; for (let row = 0; row < matrix.length; row++) { for (let col = 0; col < matrix[0].length; col++) { sum += matrix[row][col]; } } console.log('sum', sum); console.timeEnd('良好的局部性'); })();
(function worseVersion() { console.time('坏的局部性'); let sum = 0; for (let col = 0; col < matrix[0].length; col++) { for (let row = 0; row < matrix.length; row++) { sum += matrix[row][col]; } } console.log('sum', sum); console.timeEnd('坏的局部性'); })(); console.log('end');}();

还是那句话,重要的一点是:在保证你写的程序正确的前提下,你需要有一个很好的局部总结。 由于文章篇幅的限制,本文对高性能代码背后原理的讲解只能算是给读者一个粗略的品味。 有很多东西没有讨论,有些优化涉及到计算机各个层面的知识,比如计算机的利用率只是宏观层面的,只是定性分析,但是对于局部性原则,它本质上是建立在一套扎实的理论之上,依赖于计算机各个层次的良好分工,并且这种优化是可以量化分析的。

您的项目需求

*请认真填写需求信息,我们会在24小时内与您取得联系。