前一周在一席上面看了一场演讲叫「不正经历史研究所所长」,整场演讲都伴随着欢声笑语,讲的是民间文化与经典文化,经典文化实际上是统治阶级强行推动的精致文化,绝大部分老百姓是看不懂这些东西的,反正我自己参观博物馆,如果没有讲解,我是完全看不懂那些玩意的。
能和生活联系在一起的文化最有活力,比如天天发杨超越祈求不挂科,拜拜马云希望有更多的钱等等。有一句老话叫「人活一口气」,民间文化都基于此,我在别人面前要抬得起头,要的是气派、要的是大气,上个世纪的「四大件」应该就是成功的一个标配,老百姓置办这些物件后,在邻居面前也更能昂首挺胸了。
昨天了解到一个朋友家乡的有一个非常奇怪的习俗,那就是「坟头蹦迪」,我最开始的想法是,这是不是最近兴起来的习俗,但是一问才知道,他小时候就是这样的了。基于我们的体系是无法理解坟头蹦迪的,你能说它们对于亲人的去世就不伤心吗?他们要的还是能抬起头,来参加追悼会的人们看到这么豪华的场面,第一反应是故人后代是很有成就的,在某种程度上这算是对故人的颂扬。
花了几个小时时间把微信清理了一下,只留下了 83 个公众号,这其中还包括一信用卡一类的服务号,也就是说只留下了 70 个左右的公众号。留下的都是小而美的公众号,这些大佬作者的更新频率也很低,比如子柳老师最近才更新了一篇文章,而上一篇文章的更新时间是 7 月 25 日。
最近在自己身上认识的一个问题是,关注的公众号太多读不过来,虽然在此之前已经筛选掉了很多号,然而自己还是读不过来,所以索性就做了减法。现在像公众号这些,已经变成了获取信息的一个重要渠道,我也确实比周围小伙伴掌握信息更快,大部分时间也自认为比他们更了解真相。
但是我最近突然有个疑问,我真的比他们更了解真相吗?大部分人是通过垃圾文章获取信息,我虽然没有跟着垃圾文章人云亦云,但是我还是通过网络获取信息的,那么我是不是看到的是另外一种看起来更接近真相的假象呢?
这些信息其实我就算不知道好像也没有什么大碍,顶多是别人谈什么新闻时,我所了解的也就他们知道的那么点而已,更好的方式是让自己的大脑去思考,以时间的维度去追踪事件的发展。
顺便推荐一个应用叫「刷屏」,我发现自己通过「刷屏」、「知识星球」、「微信公众号」、「朋友圈」四个地方,已经掌握大部分信息了,经常同事给我说某某新闻,而我在两三天前已经见过了。
我对国际计量大会修改「1 千克」的定义这件事印象很深,但是这件事我只看到在阮一峰老师的文章中有被提到过;人们更喜欢听自己了解的领域,跟自己同好的很容易产生好感,一些明星结婚、出轨竟然能把微博的服务给搞挂了,说明绝大部分人还是更喜欢撩自己 G 点的信息。
另外第二个问题是自己现在比在学校时更喜欢玩手机了,在学校时还能做到不带手机去自习,现在是隔一会儿就看看手机,把本来就碎片化的时间变得更加碎片化了,这种效率导致工作、学习效率低下,所以把微信和 TIM 的通知全部关掉了。
写到这里发现有点点像总结,索性就给自己简单总结总结吧。2018 我从校园走向了社会,完成了从学生到职场的转变。最大的改变是思维的提升,知道去投资自己,在学生时代,愿意花几百块钱去买课程,这对我来说是很大的突破,和现在愿意花几百的概念完全不一样,想想那时候一个月生活费总共也没有多少,而我去看了下自己在毕业前花在这方面的钱居然有一千多,如果加上毕业后的开销,那就是两千多了,真感谢那时候的自己。
这其中的收获是巨大的,后面我偶尔会向朋友推荐一些好的付费课程,但是他们都和大部分人一样,吃一顿饭花几百块钱,而如果花点钱买一堂课提升一下自己,就好像要他命一样,所以后面就不和他们讲了。
2018 开始用文字记录技术、生活、感悟,这其中的收获也是不小的,认识了此前只能仰望的大佬,结交了志同道合的朋友,而且也让自己更愿意去思考了。收到电子工业出版社的约稿合同,但是现在书都没写完,明年还写不完的话,那就不写了,主要是懒。
自负的缺点已经在渐渐改变了,更加懂得了谦虚。眼界不再局限于技术,很多东西我都会去了解,也结交了很多有趣的人,初入职场,好好学习与不同的人沟通。以后如果有能力,希望能给山区带去一点点教育资源。
2019 依旧坚持每周和同事或者校友打一次羽毛球,常去爬爬山,和有趣的朋友一起疯一疯;多读书,用豆瓣等工具去筛选好书,加大阅读量;常输出,输出是更高层次的输入;尝试去了解金融的逻辑;学习新的技术领域。
Read More ~
2018 年个人总结
这个总结写的还算认真,回头看,我真的运气很好,遇到了很多大佬,在我还未毕业时并没有嫌弃我傻,教我的不仅仅是技术,还有理财、为人处世,下面是我这一年的成长经历。
有一段时间因为华为 34 岁以上员工被裁、中兴程序员跳楼等事件的发生,各种蹭热点讨论“中年危机”的文章漫天飞,那时我正忙于找工作。
当时一个微信群里面大家各种讨论中年危机,都在给自己制造焦虑,刚好群里有个大神可能看大家过于焦虑,就在群里发了几条消息,教大家如何避免中年危机,并且推荐了两本书。
还是学生的我下意识的就发了一个添加好友请求,庆幸的是他同意了我的好友请求,当然我们没有什么交流,我的问题过于浅显,我明白自己这个水平问问题,会浪费人家时间,当时的想法是看看大神的朋友圈,他平时都接触什么,自己学习一段时间。
大神推荐的书是李笑来写的《把时间当做朋友》、《财富自由之路》,两本书在学校图书馆都没有,我就给学校图书馆荐购系统提交了这两本书,图书馆效率也挺高,不到一周就把书给买回来了,我立马就借回来阅读。
书中的内容刷新了我以前狭隘的认知,自己从来没有像书中那样考虑问题,除了对作者的佩服之外,更多的是思考自己这种学生思维局限性太大了,要慢慢的将它摒弃。
有个定律是你关心什么就会来什么,后面陆续碰到几位像大神一样的人士,并加了他们的微信,但是都仅仅是通过他们朋友圈的蛛丝马迹去找知识,通过他们朋友圈的分享内容,我知道了“简七理财”、“码农翻身”公众号,然后知道了《富爸爸穷爸爸》、《小狗钱钱》,于是我通过微信读书,读完了这两本书,逐渐培养了理财理念。
后来没隔多久,简七出书了,我第一时间就买了她写的《好好赚钱》(同期还有刘大也出了《码农翻身》一书,我也第一时间买了),简七写的内容通俗易懂,很容易理解。
刘大在群里开了几次公开课,作为计算机专业的我,被刘大对技术的理解之深给折服了,正是业界浮躁的时候,成千上万人想着人工智能、大数据、区块链,而刘大一直能沉下心来去了解技术的原理,这给了我一个很好的榜样,我也逐渐沉下心来,开始去补最基础的知识,像《深入理解计算机系统》一类书也能尽下心来慢慢去啃(当时没啃完,最近又在啃),这种不浮躁的特质对我的技术成长是很有帮助的。
此后有一天,另一个大神在朋友圈分享了曹大写的《从校园到职场系列文章》,喜欢深入挖掘信息的我,以曹大公众号为源头,又找到了冯大、池大、二爷等人的公众号。
作为自由的大四学生,因为不用担心第二天起不来,我那段时间经常熬夜阅读他们的文章,再阅读的过程中我也开始思考自己此前哪些想法狭隘,哪些品质又是值得继续保持的。
也是那时开始接受知识付费的,那时候已经有小密圈(现在叫知识星球)了,出于对几位大佬的信任,我第一次大胆的花了几百块钱加入了刘大、曹大、冯大、程序员小灰的小密圈,其中的内容比网上蹭热点的文章好不知多少倍,一贯爱捕捉蛛丝马迹的我,又通过评论信息发现了 angela zhu、子柳老师、陈利人老师等,然后去找他们的文章,他们输出的内容要比水军写的文章好太多。
自己也是从那时候开始坚持写文章记录自己的心得的,通过写文章,我认识了很多优秀的人,比如吴小龙同学、了不起的杰克、java 小咖秀等公众号的作者,和他们交流的很少,但是却很受用,他们的积极向上也影响着我一直保持着乐观豁达的心态。
自己写的文章也被几个资深程序员赞同,同时还收到了两个出版社发来的出书邀请,让我体会到了无心插柳柳成荫的收获。
让我坚持一直写文章的动力不是赚钱,而是我切切实实体会到了它给我个人带来的成长,为了自己日后再看时能立刻就找到清晰的逻辑,我把都尽可能把文章写得有理有据,掌握自己的节奏,尽量提高文章质量。此前写的谈一下写作的重要性一文有说写作可以带来的好处。
现在已经不把自己当新人了,而且有同龄人甚至比我年龄还大的人向我咨询问题时,我也能给出合理建议,都得到了他们的肯定。最近发现和周围伙伴最明显的一个区别就是,对于同一个新闻,我经常早于他们半天甚至一两天知道,而且掌握的信息比他们还准确,我认为这就是整体认知水平的提升。
想说的是,执行力与信息素养很重要,执行力强的人会与你拉开越来越大的距离,信息素养也是一个关键品质,现在网络上充斥着大量的虚假信息,如何去分别这些信息的真假,在相同条件下如何获得更多的有效信息,是必备的能力。
当前年龄 23,刚大学毕业几个月,没读研。按十年为期给自己定了几个小小的目标:
父母是地地道道的农民,智能手机都不会用,十年之类给自己和父母把重疾险、意外险之类的保险配置齐全,虽然父辈一直反对买保险。
提高获取信息的能力,虽然现在对信息的掌握都比周边伙伴要早半天至几天,但是都不是自己的分析结果,学习以时间的纬度跟踪事件的发展。
学习理财知识,现在只对信用卡、基金有一点点的了解,不管炒不炒股,金融知识都还是要学的,这方面通过看书、阅读、小额实操学习。
提升自己的技术实力,职业是程序员,前后端都做,但是自己对技术的热情不是多么高涨(至少比身边一半人要高涨),以我对自己的了解,我在技术的道路上成长为小公司一个的架构师应该不成问题,再高层级怕是不行。
慢慢做到不止一份收入来源,这方面不是多清晰,现在每个月平均会有 200 左右的非工资收入(帮助别人时发的红包等),十年后做到其它收入基本和工资持平。不至于因为钱的问题而忍受心中的不快,至少得有能指着老板的鼻子说“老子不干了”的底气。
世界那么大,应该去看看,国内除了西北地区,中国很多地方已经留下了我的足迹,旅游不仅仅是玩耍,更是提升见识、获得灵感的有效途径,十年至少得把自己的脚印印到 5 个国家的土地上吧。
十年之后应该已经结婚了,房子是现在最遥不可及的目标,但是心里莫名有一股自信,这个后面会实现的,虽然不知道哪里来的这股自信。
最后一个,趁年轻,多学习,做一个终身学习的人,时刻保持学习的态度,多做有利于他人的事,现在水平不高,我能帮助到的大部分都是硕士及以下。努力提高自己,帮助更多的人。更大的目标是能给山区学校带去一些更好的教育资源。
Read More ~
匆匆的岁月——成都七中网校远端学生的高中生活
周三刷星球时看到一条关于成都七中网校的信息,没想到周四就被成都七中网校给刷屏了,看到文章里面的描写,感悟颇多,网校陪伴了自己三年,一个诗意的女孩——西凉忆就是因为网校结识的。
我是 12 年入学高中,那一年学校也刚和成都七中开通网校,因此我属于学校第一届全程使用网校资源的学生。记得刚入学时,对这种教学方式并不适应。不止学生不适应,老师也不适应,政治课老师就不使用直播课程,而是选择自己给我们讲。不过后来年级组出了硬性规定,特优班必须使用直播课程。
就像教育的水平线中描述的一样,我们被称为远端学生,大家都是第一次使用这样的系统,七中老师估计也很好奇,都在第一节课或者第二节课上抽了远端同学回答问题,后来就很少有抽远端同学回答问题了,估计是因为远程视频效果不好还浪费时间。
成都七中本部的学生上课很有激情,一到讨论等环节,虽然很吵但是却很有秩序,而我们这边基本是大家都盯着屏幕,等待对方讨论结束。
对方学生的基础我们是没办法比的,大部分能够完全以纯英文交流,而我们远端学生大部分都只能说出“My name is ..., I'm from ..., I like ...”,英语的差距是最大的。我自己是主动找了英语老师谈话,和她做了一个约定,每周我写一篇作文交给她,她帮我批改,这样坚持了两年。
让我感受到最大差距的是,一个中学有自己的电视台,经常会有像《汉语桥》冠军一类的人物前来演讲,美国第一夫人米歇尔也到七中演讲(那是总统还是奥巴马),作为远端学生有幸蹭了一场名人演讲;七中学生的寒暑假是参加联合国模拟大会、到哈佛做了简短的交流、到华盛顿旅行......
而大部分远端的学生,要么是参加学校组织的补课,要么是在家干农活,基本连县城都没有走出去过,和七中相比,完全是天壤之别。
现在我依旧还清晰的记得七中的几个老师,做 PPT 从来没有背景图的数学老师,语速超快但又吐字清晰的化学老师,说着一口标准川普的物理老师,有着一头蓬松金黄卷发的历史老师(男)......去看这些老师的背景,也都是名校出身,武汉大学、华中师大等等。
有一个细节记得很清楚,在一堂数学课中有个题目,题目具体内容忘了,只记得是要求算新华中学的一本录取率,最后的答案是在 74% 左右,这个数字在我眼里很高了,但是那一刻并没有觉得有什么,毕竟这是书上的学校嘛!!
想不到的是,下一秒那个做 PPT 从来没有背景图的数学老师说了句:“这个就比较低了啊,我们学校考的很差的时候才是百分之七十多的录取率”。一下让我震惊了,因为我在心里算过自己学校的一本录取率,在此之前不到 10%,而且我所在高中在所有远端学校中还是比较靠前的。
让我意外的是,七中的老师原来也会骂人、打人,即使打的很轻;学生没完成作业,也会被罚拿个小凳子到教室外面补作业;在全国禁止补课的规定之下,七中也会给高三的学生补课,当然我们也同步补课了。
我无法构想如果三年没有使用网校资源会是神马结果,如果仅仅是看数据的话,一本率是按倍数翻的,12 年开始使用网校资源后,学校有了第一个清华,13 年又添了一个清华。我属于 14 届,这届没有清华,最好的是浙大,我进了个普普通通的哈尔滨工程大学。据说 15 届又出了清华。
我所在的高中也被央视作为典型案例探讨农村高考。无疑能上央视,那这个学校在当地乃至全国同等水平的学校中是很成功的。
无疑这种生活给每个同学都留下了难忘的记忆,如果哪位同学加了七中本部直播班级某位同学的 QQ(那时没有人用微信),那能吹好几天牛逼,七中人在我们眼里就像明星一样。
我们当地 14 届县状元高考分数是 635 分,七中本部平均分是 616 分,这差距至今都让我目瞪口呆。前段时间曹大在星球发起了一个作业:十年后的期望目标。我所提交的作业中写了句,如果有能力,希望能给乡村学校带去一点教育资源。
我并不认为穷是光荣的事情,但在很多农村人眼里穷是一种资本,一种获取国家福利的资本,如果某次补助没有评到自己头上,那得记恨村长一辈子。我认为这才是造成农村孩子和城里孩子有巨大差距的原因,如果孩子没有从这种思维走出来,那一辈子也不可能有什么大的成就。
没想到自己都大学毕业了,却看到成都七中的网校被刷屏了。毫无疑问,这是一件极为有意义的事情,这种内容就应该被刷屏,愿七中和远端学校都越办越好。
文中照片取自于同学 QQ 空间,记得我原来还有英语老师 Spring 的 QQ,高中时向他请教过自己的英语问题,太久没联系后来 Spring 把我删了 =_=,反正是没了。
Read More ~
记录在南京大学半天
因为工作需要,到南京出差了半个月,中间利用周末和最好的朋友疯了一天,之后自己又一个人到南京大学鼓楼校区逛了逛。
不会勾搭妹子的我总是能勾搭到老爷爷,到南大就勾搭了一个 86 岁高龄的老教授,他毕业于中山大学,年轻时候是做地质工作的。
我就像个熊孩子一样要爷爷给我讲有趣的故事,要听他讲我们这一代人或者是大部分人都不知道的历史。
爷爷虽然已经是快到耄耋之年的人了,但是对年轻时候的事记得很清楚,只是对最近的事记不起来。这篇文章仅仅是记录一下爷爷所讲的趣事。
爷爷年轻时候接到中科院的任务,前往内蒙古考察。在考察期间他们用汽车压死过一只狼,而且当时吃了狼肉,一行 30 多个人都吃过那匹狼的心,但是没有吃过狗肺。
据爷爷说,狼是很狡猾的动物,他们用汽车去追狼,狼就在原地不跑,等到你离它只有 10 来米的时候,突然拐弯跑了,这样的情况他们一共遇到了 6 次。这和《狼图腾》一书中的描写基本一致,狼有先进的军事文化。
爷爷告诉我,南大起源于金陵大学,南京大学的标志性建筑「北大楼」是个教堂的样子,金陵大学本来是个教会大学,现在的「北大楼」就是原来的「钟楼」。
南大的地下有隧道,是当年毛主席提倡「深挖洞、广积粮、不称霸」时挖的,目的是为了防空。后来被南京食品公司用来存放香蕉,就是那种没有熟的香蕉,在隧道里面放熟了,再拿出来卖。不过现在隧道所有的口都没堵上了,完全废弃了。
在南大,有一些楼中间有 5 层,然后到两遍就只有 3 层了,整体看来像是个三角形。实际上这些楼当年都是要修 8 层的,因为那时候没钱,建着建着发现没资金了,所以就封顶了。
但是南大计算中心那栋楼只有 3 层却不是因为没钱,而是因为它旁边是消防大队,本来也是要建 8 层的,消防队说建高了挡住了他们视线,不能及时发现火情。爷爷笑着对我说:“但是也没见他们在上面拿个望远镜望啊!”。
我们都知道「五四运动」,但是却很少有人知道「四五运动」,这个运动的起源就在南大,当时 300 多学生(我回来查资料说是 400)发起了这个运动,后来演变为全国性的运动,直接带动了半年后四人帮被粉碎。
那是爷爷是个老师,他说他们教职工是很支持这些学生的,但是不敢公开性的支持。学生们很聪明,把标语刷到火车上,但是所有出南京的火车都被四人帮用水把标语给冲刷掉了,学生们就用沥青往火车上面写,才通过火车把这项运动的信息带到了全国各地。
我回来后查了一点资料,「四五运动」的起源是因为周恩来总理的去世,四人帮居然压制人民群众悼念周恩来,诬陷邓小平,而那时的毛主席也已经病到无法行动。
人们把花圈都放到人民英雄纪念碑前悼念周总理,却被四人帮给清理了,北京广大人民群众在“还我花圈,还我战友”的口号下行成了天安门广场大规模的群众抗议运动。
那也是一个诗意的年代,人们通过写诗来表达自己心中的愤怒,把小瓶子挂在树上,蕴意着期待邓小平的归来。那段时间四人帮应该是很难过的,从姚文元的日记就可以看出来。
爷爷还给我讲了一点他们的研究,他们研究行政规划的很多人认为,中国现在的行政划分有很多缺点的,中国应该划分 50~80 个省级单位。现在中国的行政级别也有问题,宪法规定行政层级只有三级(这一点我没查),而现在很多地方县下面是镇,镇下面还有乡,严格讲这是违宪的。
快到午饭时间时,爷爷还教我写了一会儿字,有的简体字很难看,比如「龍飛鳳舞」用繁体字写出来很好看,但是用简体字写出来就特难看。要想练好毛笔字,把三个字写好了就行了,然而我现在只记得一个“飛”字了,这可能就是老师们常说的「你又还给我了」。
Read More ~
为什么计算机处理排序数组比未排序数组快?
今天在群里看到一个有意思的问题——为什么处理排序数组比处理没有排序的数组要快,这个问题来源于 StackoverFlow,虽然我看到代码略微知道原因,但是模模糊糊不够清晰,搜了很多博客也讲的不够明白,所以就自己来总结了。
首先来看一下问题,下面是很简单的一段代码,随机生成一些数字,对其中大于 128 的元素求和,记录并打印求和所用时间。
import java.util.Arrays;
import java.util.Random;
public class Main
{
public static void main(String[] args)
{
// Generate data
int arraySize = 32768;
int data[] = new int[arraySize];
Random rnd = new Random(0);
for (int c = 0; c < arraySize; ++c)
data[c] = rnd.nextInt() % 256;
// !!! With this, the next loop runs faster
Arrays.sort(data);
// Test
long start = System.nanoTime();
long sum = 0;
for (int i = 0; i < 100000; ++i)
{
// Primary loop
for (int c = 0; c < arraySize; ++c)
{
if (data[c] >= 128)
sum += data[c];
}
}
System.out.println((System.nanoTime() - start) / 1000000000.0);
System.out.println("sum = " + sum);
}
}
我的运行结果:分别在对数组排序和不排序的前提下测试,在不排序时所用的时间比先排好序所用时间平均要多 10 ms。这不是巧合,而是必然的结果。
问题就出在那个if判断上面,在旧文顺序、条件、循环语句的底层解释中其实已经提到了造成这种结果的原因,只是旧文中没有拿出具体的例子来说明。
为了把这个问题搞明白,需要先对流水线有一定的了解。计算机是指令流驱动的,执行的是一个一个的指令,而执行一条指令,又要经过取指、译码、执行、访存、写回、更新六个阶段(不同的划分方式所包含的阶段不一样)。
六个阶段使用的硬件基本是不一样的,如果一条指令执行完再去执行另一条指令,那么在这段时间里会有很多硬件处于空闲状态,要使计算机的速度变快,那么就不能让硬件停下来,所以有了流水线技术。
流水线技术通过将指令重叠来实现几条指令并行处理,下图表示的是三阶段指令时序,即把一个指令分为三个阶段。在第一条指令的 B 阶段,A 阶段相关的硬件是空闲的,于是可以将第二条指令的 A 阶段提前操作。
很明显,这种设计大幅提高了指令运行的效率,聪明的你可能发现问题了,要是不知道下一条指令是什么怎么办,那提前的阶段也就白干了,那样流水线不就失效了?没错,这就是导致开篇问题的原因。
让流水线出问题的情况有三种:
数据相关,后一条指令需要用到前一条指令的运算结果;
控制相关,比如无条件跳转,跳转的地址需要在译码阶段才能知道,所以跳转之后已经被取出的指令流水就需要清空;
结构相关,由于一些指令需要的时钟周期长(比如浮点运算等),长时间占用硬件,导致之后的指令无法进入译码等阶段,即它们在争用同一套硬件。
代码中的if (data[c] >= 128)翻译成机器语言就是跳转指令,处理器事先并不知道要跳转到哪个分支,那难道就等知道了才开始下一条指令的取指工作吗?处理器选择了假装知道会跳转到哪个分支(不是谦虚,是真的假装知道),如果猜中了是运气好,而没有猜中那就浪费一点时间重新来干。
没有排序的数组,元素是随机排列的,每次data[c] >= 128的结果也是随机的,前面的经验就不可参考,所以下一次执行到这里理论上还是会有 50% 的可能会猜错,猜错了肯定就需要花时间来修改犯下的错误,自然就会浪费更多的时间。
对于排好序的数组,开始几次也需要靠猜,但是猜着猜着发现有规律啊,每次都是往同一个分支跳转,所以以后基本上每次都能猜中,当遍历到与 128 分界的地方,才会出现猜不中的情况,但是猜几次之后,发现这又有规律啊,每次都是朝着另外一个相同分支走的。
虽然都会猜错,但是在排好序的情况下猜错的几率远远小于未排序时的几率,最终呈现的结果就是处理排序数组比未排序数组快,其原因就是流水线发生了大量的控制相关现象,下面通俗一点,加深一下理解。
远在他方心仪多年的姑娘突然告诉你,其实她也喜欢你,激动的你三天三夜睡不着觉,决定开车前往她的城市,要和她待在一起,但是要去的路上有很多很多岔路,你只能使用的某某地图导航,作为老司机并且怀着立马要见到爱人心情的你,开车超快,什么样罚单都不在乎了。
地图定位已经跟不上你的速度了,为了尽快到达,遇到岔路你都是随机选一条路前进,遗憾的是,自己的选择不一定对(我们假设高速可以回退),走错路了就要重新回到分岔点,这就对应着未排序的情况。
现在岔路是有规律的,告诉你开始一直朝着一边走,到某个地点后会一直朝着另一边走,你只需要花点时间去探索一下开始朝左边还是右边,到了中间哪个地点会改变方向就可以了,相比之下就能节省不少时间了,尽快见到自己的爱人,这对应着排好序的情况。
最后的故事改编自两个人的现实生活,一位是自己最好的朋友之一,谈恋爱开心的睡不着觉;另一位是微信上的一位好友,为了对方从北京裸辞飞到了深圳。
Read More ~
顺序、条件、循环语句的底层解释(机器语言级解释)
初级程序员讲究的是术,只知道如何用关键字去拼凑出领导想要的功能;高级程序员脸的是道,理解了底层的逻辑,不仅把功能做的漂漂亮亮,心法也更上一层楼。
顺序结构
数据传送指令
我们都清楚,绝大多数编译器都把汇编语言作为中间语言,把汇编语言程序变成可运行的二进制文件早就解决了,所以现在的高级语言基本上只需要把自己翻译成汇编语言就可以了。
汇编指令总共只有那么多,大多数指令都是对数据进行操作,比如常见的数据传送指令mov。不难理解,被操作数据无非有三种形式,立即数,即用来表示常数值;寄存器,此时的数据即存放在指定寄存器中的内容;内存引用,它会根据计算出来的地址访问某个内存位置。
需要注意的是,到了汇编层级,就不像高级语言那样随随便便int就能和long类型的数据相加减,他们在底层所占有的字节是不一样的,汇编指令是区分操作数据大小的,比如数据传送指令,就有下面这些品种(x86-64 对数据传送指令加了一条限制:两个操作数不能都指向内存位置)。
压栈与弹栈
对于栈,我想不必多讲,IT 行业的同学都清楚,它是一种线性数据结构,其中的数据遵循“先进后出”原则,寄存器%rsp保存着栈顶元素的地址,即栈顶指针。一个程序要运行起来,离不开栈这种数据结构。
栈使用最多的就是弹栈popq和压栈pushq操作。比如将一个四字值压入栈中,栈顶指针首先要减 8(栈向下增长),然后将值写到新的栈顶地址;而弹栈则需要先将栈顶数据读出,然后再将栈指针加 8。所以pushq和popq指令就可以表示为下面的形式。
// 压栈
subq $8, %rsp
movq %rbp, (%rsp)
// 弹栈
movq (%rsp), %rax
addq $8, %rsp
其他还有算术、逻辑、加载有效地址、移位等等指令,可以查阅相关文档了解,不作过多介绍,汇编看起来确实枯燥乏味。
条件结构
前面讲的都是顺序结构,我们的程序中不可能只有顺序结构,条件结构是必不可缺的元素,那么汇编又是如何实现条件结构的呢?
首先你需要知道,除了整数寄存器,CPU 还维护着一组条件码寄存器,我们主要是了解如何把高级语言的条件结构转换为汇编语言,不去关注这些条件码寄存器,只需要知道汇编可以通过检测这些寄存器来执行条件分支指令。
if-else 语句
下面是 C 语言中的if-else语句的通用形式。
if(test-expr){
then-statement
}else{
else-statement
}
汇编语言通常会将上面的 C 语言模板转换为下面的控制流形式,只要使用条件跳转和无条件跳转,这种形式的控制流就可以和汇编代码一一对应,我们以 C 语言形式给出。
t = test-expr;
if(!t){
goto false;
}
then-statement;
goto done;
false:
else-statement;
done:
但是这种条件控制转移形式的代码在现代处理器上可能会很低效。原因是它无法事先确定要跳转到哪个分支,我们的处理器通过流水线来获得高性能,流水线的要求就是事先明确要执行的指令顺序,而这种形式的代码只有当条件分支求值完成后,才能决定走哪一个分支。即使处理器采用了非常精密的分支预测逻辑,但是还是有错误预测的情况,一旦预测错误,那将会浪费 15 ~ 30 个时钟周期,导致性能下降。
在流水线中,把一条指令分为多个阶段,每个阶段只执行所需操作的一小部分,比如取指令、确定指令类型、读数据、运算、写数据以及更新程序计数器。流水线通过重叠连续指令的步骤来获得高性能,比如在取一条指令的同时,执行它前面指令的算术运算。所以如果事先不知道指令执行顺序,那么事先所做的预备工作就白干了。
为了提高性能,可以改写成使用条件数据传送的代码,比如下面的例子。
v = test-expr ? then-expr : else-expr;
// 使用条件数据传送方法
v = then-expr;
ve = else-expr;
t = test-expr;
if(!t){
v = ve;
}
这样改写,就能提高程序的性能了,但是并不是所有的条件表达式都可以使用条件传送来编译,一般只有当两个表达式都很容易计算时,编译器才会采用条件数据传送的方式,大部分都还是使用条件控制转移方式编译。
switch 语句
switch语句可以根据一个整数索引值进行多重分支,在处理具有多种可能结果的测试时,这种语句特别有用。为了让switch的实现更加高效,使用了一种叫做跳转表的数据结构(Radis 也是用的跳表)。跳转表是一个数组,表项 i 是一个代码段的地址,当开关情况数量比较多的时候,就会使用跳转表。
我们举个例子,还是采用 C 语言的形式表是控制流,要理解的是执行switch语句的关键步骤就是通过跳转表来访问代码的位置。
void switch_eg(long x, long n, long *dest){
long val = x;
switch(n){
case 100:
val *= 13;
break;
case 102:
val += 10;
case 103:
val += 11;
break;
case 104:
case 105:
val *= val;
break;
default:
val = 0;
}
*dest = val;
}
要注意的是,上面的代码中有的分支没有break,这种问题在笔试中会经常遇到,没有break会继续执行下面的语句,即变成了顺序执行。上面的代码会被翻译为下面这种控制流。
void switch_eg(long x, long n, long *dest){
static void *jt[7] = {
&&loc_A, &&loc_def, &&loc_B,
&&loc_C, &&loc_D, &&loc_def,
&&loc_D
};
unsigned long index = n - 100;
long val;
if(index > 6){
goto loc_def;
}
goto *jt[index];
loc_A:
val = x * 13;
goto done;
loc_B:
x = x + 10;
loc_C:
val = x + 11;
goto done;
loc_D:
val = x * x;
goto done;
loc_def:
val = 0;
done:
*dest = val;
}
循环结构
C 语言中有do-while、while和for三种循环结构,它们的通用形式一般都长下面那样。
// do-while
do
body-statement
while(test-expr);
// while
while(test-expr)
body-statement
// for
for(init-expr; test-expr; update-expr)
body-statement
do-while的特点是body-statement一定会执行一次,所以我们可以将do-while翻译成下面的控制流形式,很容易就能联想到它的汇编形式。
loop:
body-statement;
t = test-expr;
if(t){
goto loop;
}
while循环我们给出两种形式的控制流,其中一种包含do-while形式,如下所示。
// 第一种形式
t = test-expr;
if(!t){
goto done;
}
do
body-statement;
while(test-expr);
done:
// 第二种形式
goto test;
loop:
body-statement;
test:
t = test-expr;
if(t){
goto loop;
}
面试的时候,有的面试官会问你for循环的执行顺序,现在深入理解了三种循环的机制,再也不怕面试官啦。for循环可以转换成如下的while形式。
init-expr;
while(test-expr){
body-statement;
update-expr;
}
有了这种形式的for循环,我们只需要将其中的while部分再翻译一下就好了,前文给出了两种while翻译的方式,而具体采用哪种方式,取决于编译器优化的等级。
总结
计算机就是用那么几条简简单单的指令就完成了各种复杂的操作,不得不折服于计算机科学家们的魅力。现在人工智能被炒的很火热,然后人是事件、情感驱动的,而计算机是控制流驱动的,所以从架构上就决定了,冯诺依曼体系计算机实现的都是弱人工智能。
Read More ~
深入理解计算机系统——函数调用与空间分配
我们在编程序的时候,都会把某一个特定功能封装在一个函数里面,对外暴露一个接口,而隐藏了函数行为的具体实现,一个大型的复杂系统里面包含了很多这样的小函数,我们称之为过程。
过程是相对独立的小模块,系统的运行需要这些过程的紧密合作,这种合作就是函数调用。
在一个函数执行时调用别的函数,比如 P 调用 Q,需要执行一些特定的动作。传递控制,在调用 Q 之前,控制权在 P 的手里,既然要调用 Q,那么就需要把控制权交给 Q;传递数据,就是函数传参;分配与释放内存,在开始时,Q 可能需要位局部变量分配空间,结束时又必须释放这些存储空间。
大多数语言都使用栈提供的先进后出机制来管理内存,x86-64 可以通过通用寄存器传递最多 6 个整数值(整数或地址),如果超过 6 个,那就需要在栈中分配内存,并且通过栈传递参数时,所有数据的大小都要向 8 的倍数对齐。将控制权从 P 转交给 Q,只需要将 PC(程序计数器)的值置为 Q 代码的起始位置,并记录好 P 执行的位置,方便 Q 执行完了,继续执行 P 剩余的代码。
在函数的传参、执行中,多多少少都需要空间来保存变量,局部数据能保存在寄存器中就会保存在寄存器中,如果寄存器不够,将会保存在内存中。除了寄存器不够用的情况,还有数组、结构体和地址等局部变量都必须保存在内存中。分配内存很简单,只需要减小栈指针的值就行了,同样释放也只需要增加栈指针。
在函数执行过程中,处理栈指针%rsp,其它寄存器都被分类为被调用者保存寄存器,即当过程 P 调用过程 Q 时,Q 必须保存这些寄存器的值,保证它们的值在 Q 返回到 P 时与 Q 被调用时是一样的。
所以递归也就不难理解了,初学算法总觉得递归有点奇妙,怎么自己调用自己,而实际上对于计算机来说,它和调用其它函数没什么区别,在计算机眼里,没有自身与其它函数的区别,所有被调用者都是其它人。
数组是编程中不可或缺的一种结构,“数组是分配在连续的内存中”这句话已经烂熟于心了,历史上,C 语言只支持大小在编译时就能确定的多维数组,这个多多少少有一些不便利,所以在ISO C99标准中就引入了新的功能,允许数组的维度是表达式。
int A[expr1][expr2]
因为数组是连续的内存,所以很容易就能访问到指定位置的元素,它通过首地址加上偏移量即可计算出对应元素的地址,这个偏移量一定意义上就是由索引给出。
比如现在有一个数组A,那么A[i]就等同于表达式* (A + i),这是一个指针运算。C 语言的一大特性就是指针,既是优点也是难点,单操作符&和*可以产生指针和简介引用指针,也就是,对于一个表示某个对象的表达式expr,&expr给出该对象地址的一个指针,而对于一个表示地址的表达式Aexpr,*Aexpr给出该地址的值。
即使我们创建嵌套(多维)数组,上面的一般原则也是成立的,比如下面的例子。
int A[5][3];
// 上面声明等价于下面
typedef int row3_t[3];
row3_t A[5];
这个数组在内存的中就是下面那个样子的。
还有一个重要的概念叫做数据对齐,即很多计算机系统要求某种类型的对象的地址必须是某个值 K(一般是2、4 或 8)的倍数,这种限制简化了处理器和内存接口之间的设计,甚至有的系统没有进行数据对齐,程序就无法正常运行。
比如现在有一个如下的结构体。
struct S1 {
int i;
char c;
int j;
}
如果编译器用最小的 9 字节分配,那么将是下面的这个样子。
但是上面这种结构无法满足 i 和 j 的 4 字节对齐要求,所以编译器会在 c 和 j 之间插入 3 个字节的间隙。
在极客时间专栏中有这样一段代码。
int main(int argc, char *argv[]){
int i = 0;
int arr[3] = {0};
for(; i <= 3; i++){
arr[i] = 0;
printf("Hello world!\n");
}
return 0;
}
这段代码神奇的是在某种情况下会一直循环的输出Hello world,并不会结束,在计算机系统漫游(补充)中也提到过。
造成上面这种结果是因为函数体内的局部变量存在栈中,并且是连续压栈,而 Linux 中栈又是从高向低增长。数组arr中是 3 个元素,加上 i 是 4 个元素,刚好满足 8 字节对齐(编译器 64 位系统下默认会 8 字节对齐),变量i在数组arr之前,即i的地址与arr相邻且比它大。
代码中很明显访问数组时越界了,当i为 3 时,实际上正好访问到变量i的地址,而循环体中又有一句arr[i] = 0;,即又把i的值设置为了 0,由此就导致了死循环。
Read More ~
认知如何提升?我是怎样转变自己思维方式的
这仅仅是记录自己的一点经历和感悟,回顾一下自己思维的转变过程而已,如果对于还是学生或是初入职场的你有一点帮助,那也是没白写的。
相信很多朋友都还记得有段时间因为华为 34 岁以上员工被裁、中兴程序员跳楼等事件的发生,各种蹭热点讨论「中年危机」的文章漫天飞,一时间各种割韭菜教如何利用副业赚钱的课程也层出不穷,那时我正好是大四,忙于找工作。
很清晰的记得当时一个程序员微信群里面大家各种讨论中年危机,都在给自己制造焦虑,刚好群里有个大神可能出于善意,看大家过于焦虑,就在群里发了几条消息,教大家如何避免中年危机,并且推荐了两本书。
还是学生的我下意识的就发了一个添加好友请求,虽然内心是非常希望对方能够通过好友请求的,但当时很清楚能和这样的人微信交流是一种奢求,而意外的是他居然同意了我的好友请求,当然限于我个人的水平我们没有什么交流,我的问题过于浅显,会浪费人家时间,当时的想法是看看大神的朋友圈,他平时都接触什么,自己学习一段时间。
大神推荐的书是李笑来写的《把时间当做朋友》、《财富自由之路》,那时的心态还是宁愿花 300 块钱出去吃一顿饭,也不愿意花几十块钱买一本书,所以我第一时间跑到学校图书馆去查了,但是两本书在学校图书馆都没有,我就给学校图书馆荐购系统提交了这两本书,图书馆效率也挺高,不到一周就把书给买回来了,我立马就借回来阅读。
书中的内容刷新了我以前狭隘的认知,从偏远农村出来的自己从来没有像书中那样考虑问题,除了对作者的佩服之外,更多的是思考自己这种学生思维局限性太大了,要慢慢的将它摒弃。
有个定律是你关心什么就会来什么,后面陆续碰到几位像大一样的人士,并加了他们的微信,但是都仅仅是通过他们朋友圈的蛛丝马迹去找知识,通过他们朋友圈的分享内容,我知道了「简七理财」、「码农翻身」、「程序员小灰」(我知道的时候还不叫程序员小灰)公众号,然后知道了《富爸爸穷爸爸》、《小狗钱钱》,通过微信读书,读完了这两本书,逐渐培养了理财理念。
后来没隔多久,简七出书了,我第一时间就买了她写的《好好赚钱》(同期还有刘大也出了《码农翻身》一书,我也第一时间买了),简七写的内容通俗易懂,很容易理解。
刘大在群里开了几次公开课,作为计算机专业的我,被刘大对技术的理解之深给折服了,那段时间正是业界浮躁的时候,成千上万人想着人工智能、大数据、区块链,而刘大一直能沉下心来去了解技术的原理,这给了我一个很好的榜样,我也逐渐沉下心来,开始去补最基础的知识,像《深入理解计算机系统》一类书也能尽下心来慢慢去把它啃完了,这种不浮躁的特质对我的技术成长是很有帮助的。
此后有一天,另一个大神在朋友圈分享了曹大写的《从校园到职场系列文章》,喜欢深入挖掘信息的我,以曹大公众号为源头,又找到了冯大、池大、二爷、刘备教授、大牛猫等人的公众号。
作为自由的大四学生,因为不用担心第二天起不来,我那段时间经常熬夜阅读他们的文章,在阅读的过程中我也开始思考自己此前哪些想法狭隘,哪些品质又是值得继续保持的。
也是那时我开始接受知识付费的,那时候已经有小密圈(现在叫知识星球)了,出于对几位大佬的信任,我第一次大胆的花了几百块钱加入了刘大、曹大、冯大、程序员小灰的小密圈,其中的内容比网上蹭热点的文章好不知多少倍,一贯爱捕捉蛛丝马迹的我,又通过评论信息发现了 angela zhu、子柳老师、陈利人老师等,然后去找他们的文章,他们输出的内容要比水军写的文章好太多。
自己也是从那时候开始坚持写文章记录自己的心得的,通过写文章,也认识了很多优秀的人,比如了不起的杰克、java 小咖秀等公众号的作者,和他们交流的很少,但是却很受用,他们的积极向上也影响着我一直保持着乐观豁达的心态。
自己写的文章也被几个资深程序员赞同,同时还收到了两个出版社发来的出书邀请,让我体会到了无心插柳柳成荫的收获,选择了和电子工业出版社签了出版合同。
让我坚持一直写文章的动力不是赚钱,而是我切切实实体会到了它给我个人带来的成长,为了自己日后再看时能立刻就找到清晰的逻辑,我把都尽可能把文章写得有理有据,掌握自己的节奏,尽量提高文章质量。谈一下写作的重要性一文有说写作带来的好处。
现在已经不把自己当新人了,而且有同龄人甚至比我年龄还大的人向我咨询问题时,我也能给出合理建议,都得到了他们的肯定。最近发现和周围伙伴最明显的一个区别就是,对于同一个新闻,我经常早于他们半天甚至一两天知道,而且掌握的信息比他们还准确,我认为这就是整体认知水平的提升。
最后想说,执行力与信息素养很重要,执行力强的人会与你拉开越来越大的距离,信息素养也是一个关键品质,现在网络上充斥着大量的虚假信息,如何去分别这些信息的真假,在相同条件下如何获得更多的有效信息,是必备的能力。
上面提到的各路大神,他们的文章都很值得阅读,做一个终身学习的人,时刻保持学习的态度。
Read More ~
深入理解计算机系统——信息的表示与处理
参考内容:
《深入理解计算机系统》(第三版)
字数据大小
前面已经提到过信息=位+上下文,但是基本上的计算机都没有将位作为最小的可寻址单位,而是将字节作为了最小的可寻址单位,内存就是一个非常大的字节数组,它的的每个字节都由一个唯一的数字来标识(这个数字是不需要存的),所有可能的地址集合就是虚拟地址空间。
我们常说的 32 位、64 位指的是一台计算机的字长,用于指明指针数据的的标称大小。有的面试官在面试的时候会问这样一个问题:在 C/C++ 中指针的大小是多少?如果你一下就回答出来时多少个字节了,那基本上不必再问了,因为一个指针的大小取决于计算机的字长,所以应该分 32 位机还是 64 位机的情况。
字长还会决定一个极为重要的系统参数——虚拟地址空间。比如现在有一个 32 位机,每一位可以取值 1 或 总共 32 位,能组合的出局就有 232 个,所以它能访问 232 个地址,其大小也就是 4G,因此你如果给 32 位机装上 8G 的内存条,是起不了多大作用的。
我们平时所说的 32 位程序和 64 位程序并不是指机器的字长,它们的区别在于程序时如何编译的,而不是其运行的机器类型,高版本都应该做到向后兼容,所以 32 位程序一般都能运行在 64 位机器上,而 64 位程序时不能运行在 32 位机上面的。下面两种伪指令就分别用于编译 32 位程序和 64 位程序。
gcc -m32 prog.c
gcc -m64 prog.c
C 语言在 32 位机和 64 位机上所表现的差别在于long数据类型,一般在 32 位机上是 4 个字节,而在 64 位机上是 8 个字节,而作为程序员要力图程序能在不同的机器上进行编译执行,要做到这一点就需要保证程序对不同数据类型的确切大小不敏感。
曾经某运营商的一个基站版本因为数据范围的不同而造成了巨大的损失,在编程环境中使用的是 32 位机,而基站所使用的处理器没有 32 位,最后表现的效果就是大概每隔 40 天,基站就自动复位了。定位到这个问题都花费了巨大的财力和人力资源。
寻址及字节顺序
上文已经提到,有很多的对象实际上不止占用一个字节,而是占用了多个字节,此时就涉及到如何排列这些字节了,以及如何存储这些字节。以11001100 11001100为例,它占用了两个字节,我们可以选择将这两个字节放在连续的内存中,也可以将两个字节分开放在不连续的内存中;另外我们可以将左边的字节当做起始位置,也可以将右边的字节当做起始位置(更专业的称为大端法和小端法)。
对于字节的排列,到底是用大端法还是小端法,没有技术上的争论,只有社会政治论题的争论,而且机器它对程序员是完全不可见的。几乎所有的机器都将多字节对象存储为连续的字节序列,所使用字节中最小的地址作为对象的地址。
那么什么时候需要注意字节的顺序规则呢,那就是编写网络应用程序的时候,试想你传输的数据是用大端法表示的,而用户的计算机采用的是小端法,那还会有用户使用你的产品吗。所以编写网络程序时需要遵循已经建立的关于字节顺序的规则。
整数表示
程序员对二进制不会不知道,比如 11111111表示的是 255(不考虑补码),很容易就能转换为我们所熟悉的 10 进制数据。这种方式我们默认它是无符号数,如果要加入有符号数就开始变得有趣了。
几乎所有的计算机都是采用有补码来表示有符号整数的,它与无符号整数的区别在于最高位被解释为负权,举个例子:将1111看做补码的话,它的值就为:-23 + 22 + 21 + 20 = -1。
在程序中不可避免的会使用强制类型转换,C 语言中强制类型转换并没有改变数据的位值,只是改变了解释这些位的方式。比如将无符号数(unsigned) 53191 转换为有符号数的结果为 -12345,它们的位值是完全没有相同的。
最容易入坑的地方是,对两个不同类型的数据进行运算时,C 语言将会隐式的将有符号数转换为无符号数,所以就有下面这样一个神奇的结果。
// u 代表无符号数
-1 < 0u
// 结果为 0
// 因为 -1 的补码表示为:11...11
// 转换为无符号数后就是范围内最大的数
如果需要扩展一个数的位表示,那么放心的扩展就好了,小的数据类型都能安全的向大的数据类型转换,补码表示的数会在前面补上符号位,原码表示的直接在前面补上 0 即可,而需要注意的是从大往小转,这会不可避免的截断位,造成信息的丢失,所以千万不要这么干。
加法、乘法运算
在编程入门的时候可能都知道两个正数相加的结果可能为负数,还有一个更奇怪的现象就是:x < y和 x - y < 0两个表达式可能会得出不一样的结果,这些神奇的结果都和计算机整数的底层表示和运算有着密切的关系。
C 语言中有无符号数与有符号数之分,而在 Java 中只有有符号数,下面的内容还是基于 C 语言进行说明,毕竟更 C 比 Java 更接近底层嘛。
无符号加法
假设我们使用 w 位来表示无符号数,那么两个加数取值范围即为:0 ≤ x, y <2w,理论上它们的和的范围为:0 ≤ sum < 2w+1,因为只有 w 位表示无符号数(要把和表示出来就需要 w+1 位),所以超过 zw的部分就会造成溢出,如下图所示。
对于无符号数的溢出,计算机采用的处理方式是丢掉最高位,直观的结果就是,当发生溢出了,就将采用取模运算(或者说是减去 2w),举个例子。
只用 4 为来表示无符号数,即 w = 4,现在有 x [1001] 和 y [1100] 相加,其结果应为:[10101] ,但是没有 5 位用来表示,所以丢掉最高位的1,剩下的值为 5 [0101],也就是 21 mod 16 = 5。
那么如何检测是否发生溢出呢?设求和结果为 s,对于加法有 x + y ≥ x 恒成立,即只要没有发生溢出,肯定有 s ≥ x。另一方面,如果确实发生溢出了,就有 s = x + y - 2w,又有 y - 2w < 0,因此 s = x + y - 2w < x。
补码加法
和前面一样,对于两个给定范围的加数 - 2w-1 ≤ x, y ≤ 2w-1 - 1,它们的和的范围就在 - 2w ≤ sum ≤ 2w - 2。要想把整个和的范围表示出来,依旧需要 w+1 位才行,而现在只有 w 位,因此还是需要采用将溢出部分截断。
可以发现,当发生正溢出时,截断的结果是从和数中减去了 2w;而当发生负溢出时,截断结果是把和数加上 2w。
那么对于补码加法如何检测溢出结果呢?通过分析可以发现,当且仅当 x > 0, y > 0,但和 s ≤ 0 时为正溢出;当且仅当 x < 0, y < 0,但 s ≥ 0 时发生负溢出。
无符号乘法
有了前面的基础,乘法就变得简单一些了,对于溢出情况,计算机仍然采用的是求模,比如 0 ≤ x, y ≤ 2w - 1,它们乘积的范围为 0 到 22w - 2w+1 + 1 之间,这可能需要 2w 位来表示,溢出部分直接截掉,如下所示。
补码乘法
对于补码,两个乘数的范围为:- 2w-1 ≤ x, y ≤ 2w-1 + 1,那么其乘积表示范围就为 - 22w-2 + 2w-1 到 22w-2 之间,补码乘法和无符号乘法基本是一样的,只是在无符号基础上多加了一步转换,即将无符号数转换为补码。
乘以常数
我们知道,计算机做加减法、位级运算的速度最快(1 个指令周期),而做乘除法很慢(10 个甚至更多指令周期),平时编写的程序中常常会乘以一个常数,为了使程序运行的更快,编译器可能会帮我们做一些处理。
首先我们考虑常数是 2 的幂。x * 21 可以表示为 x << 1,x * 22 可以表示为 x << 2,依次类推即可。
对于不是 2 的幂的常数,比如 x * 14 可以表示为:(x<<3) + (x<<2) + (x<<1),因为 14 = 23 + 22 + 21;聪明的你可能发现 14 还有另一种表示方法,即 14 = 24 - 21,这种表示比前一种表示方法又少了运算量,所以 x * 14 还可以表示为:(x<<4) - (x<<1)。
实际上,这里有一个通用的解决方案,对于任何一个常数 K,其二进制可以表示为一组 0 和 1 交替的序列:[(0...0)(1...1)(0...0)(1...1)],14可以表示为:[(0...0)(111)(0)],考虑一组从位位置 n 到位位置 m 的连续的 1 (n ≥ m),(对于 14 有 n = 3,m = 1)可以有两种形式来计算位对乘积的影响。
这个优化不是一定的,大多数编译器只在需要少量移位、加减法就足够的时候才使用这种优化。
Read More ~
使用订阅号实现微信公众号历史文章爬虫
微信公众号已经成为生活的一部分了,虽然里面有很多作者只是为了蹭热点,撩读者的 G 点,自己从中获得一些收益;但是不乏好的订阅号,像刘大的码农翻身、Fenng的小道消息、曹大的caoz的梦呓等订阅号非常值得阅读。
平时有时候看到一些好的公众号,也会不自觉去查看该公众号的历史文章,然而每次都看不完,下一次再从微信里面打开历史文章,又需要从头翻起。而且对于写了很多年的大号,每次还翻不到底。有一些平台提供了相关的服务,但是得收几十块钱的费用,倒不是缺几十块钱,主要是觉得这种没必要花的钱不值得去浪费。
网上搜如何爬微信公众号历史文章,大致给了三种思路,第一是使用搜狗微信搜索文章,但是好像每次能搜到的不多;第二是使用抓包工具;第三种是使用个人订阅号进行抓取。
简单来说就是使用程序来模拟人的操作,抓取公众号历史文章。首先登录微信公众号个人平台,期间需要管理员扫码才能登录成功。
def __open_gzh(self):
self.driver.get(BASE_URL)
self.driver.maximize_window()
username_element = self.driver.find_element_by_name("account")
password_element = self.driver.find_element_by_name("password")
login_btn = self.driver.find_element_by_class_name("btn_login")
username_element.send_keys(USERNAME)
password_element.send_keys(PASSWORD)
login_btn.click()
WebDriverWait(driver=self.driver, timeout=200).until(
ec.url_contains("cgi-bin/home?t=home/index")
)
# 一定要设置这一步,不然公众平台菜单栏不会自动展开
self.driver.maximize_window()
进入微信公众平台首页后,点击素材管理,然后点击新建图文素材,就会进入到文章写作页面,此时前面打开的微信公众平台首页就不需要了,可以将其关闭。
def __open_write_page(self):
management = self.driver.find_element_by_class_name("weui-desktop-menu_management")
material_manage = management.find_element_by_css_selector("a[title='素材管理']")
material_manage.click()
new_material = self.driver.find_element_by_class_name("weui-desktop-btn_main")
new_material.click()
# 关闭公众平台首页
handles = self.driver.window_handles
self.driver.close()
self.driver.switch_to_window(handles[1])
在文章写作页面的工具栏上面有一个超链接按钮,点击超链接即会弹出超链接编辑框,选择查找文章,输入自己喜欢的公众号进行查找,一般第一个就是自己想要的结果,点击对应的公众号,该公众号所有的文章就会通过列表的形式展现出来。
def __open_official_list(self):
# 超链接
link_click = self.driver.find_element_by_class_name("edui-for-link")
link_click.click()
time.sleep(3)
# 查找文章
radio = self.driver.find_element_by_class_name("frm_vertical_lh").find_elements_by_tag_name("label")[1]
radio.click()
# 输入查找关键字
search_input = self.driver.find_element_by_class_name("js_acc_search_input")
search_input.send_keys(OFFICIAL_ACCOUNT)
search_btn = self.driver.find_element_by_class_name("js_acc_search_btn")
search_btn.click()
# 等待5秒,待公众号列表加载完毕
time.sleep(5)
result_list = self.driver.find_element_by_class_name("js_acc_list").find_elements_by_tag_name("div")
result_list[0].click()
文章列表已经展现出来了,直接抓取每条文章超链接的信息即可,每抓取完一页就进入下一页,继续抓取文章列表信息,直到所有文章信息都抓取完毕。
def __get_article_list(self):
# 等待文章列表加载
time.sleep(5)
total_page = self.driver.find_element_by_class_name("search_article_result")\
.find_element_by_class_name("js_article_pagebar").find_element_by_class_name("page_nav_area")\
.find_element_by_class_name("page_num")\
.find_elements_by_tag_name("label")[1].text
total_page = int(total_page)
articles = []
for i in range(0, total_page-1):
time.sleep(5)
next_page = self.driver.find_element_by_class_name("search_article_result")\
.find_element_by_class_name("js_article_pagebar").find_element_by_class_name("pagination")\
.find_element_by_class_name("page_nav_area").find_element_by_class_name("page_next")
article_list = self.driver.find_element_by_class_name("js_article_list")\
.find_element_by_class_name(" my_link_list").find_elements_by_tag_name("li")
for article in article_list:
article_info = {
"date": article.find_element_by_class_name("date").text,
"title": article.find_element_by_tag_name("a").text,
"link": article.find_element_by_tag_name("a").get_attribute("href")
}
articles.append(article_info)
next_page.click()
return articles
至此,微信公众号历史文章的爬虫已经实现,其实整个过程只不过是用程序来模拟的了人类的操作。需要注意的是,程序不能设置太快,因为微信做了相关限制,所以设太快会在一段时间内无法使用文章查找功能;另外一点是使用选择器选择页面元素的时候,会有一些坑,而且我发现不同账号登录,有很少部分的页面元素虽然直观上是一样的,但是它的 html 代码有细微的差别。
这个小程序会用到selenium库,和chromedriver,前者直接pip install即可,后者自行下载;另外你还需要一个订阅号才行,本文只实现了关键的文章信息抓取,并没有进行文章信息的持久化存储,完整代码在这里。
Read More ~