作者Hazukashiine (みなさん、こんにちは)
看板C_and_CPP
标题[闲聊] 为什麽 C 语言不提供指向虚无的指标
时间Tue Oct 10 13:28:03 2017
在 C 语言里面对 NULL 取值是非法的
通常伴随的是发生 segmentation fault
而这通常也跟 CPU 的指令集实作有关
因为编译器会将对 NULL 取值的程式码
假定 NULL 就是常数数值 0 的情况下
编译成类似 movl $10, 0x0 等的指令
因此 CPU 在处理相关指令的时候
竖立 flag 触发 OS 处理是可以预期的
但是既然 C 语言被定调成高阶语言
为什麽不提供一些抽象一些的语意
像是对「虚无指针」取值是代表忽略的意思
比如说 VACANT 代表「虚无指针」
则 void *arr[] = {VACANT, VACANT};
*arr[1] = (uintptr_t)12345;
代表什麽事(包含副作用)都不会发生
感觉对空虚取值不发生作用非常实用
就像指令集几乎都会有 NOP 一样
表面上看起来没什麽用
但是却能在不少 corner cases 发生作用
NOP 可以去解决 hazard 的问题
VACANT 可以解决不少指针初始化的问题
可以减少程式设计所需的 sentinel value
还在特定的时候能让程式减少无谓的判断式
基本上我还没想到什麽负面的影响
我的问题是为什麽不设计类似的指针?
类似 /dev/null 的概念
看起来没什麽用 但是却大大有用 XD
--
作者 sr29 (owo) 看板 Linux
标题 [问题] git add 失败
时间 Wed Jul 12 15:31:13 2017
1F:→ qsort : 我猜,该档案被锁住了,所以git无法access,才会报错07/12 15:52
2F:→ qsort : 建议用.gitignore把这类temp档ignore掉,不要上到git。07/12 15:53
3F:推 gitignore: 有人叫我?07/13 04:41
--
※ 发信站: 批踢踢实业坊(ptt.cc), 来自: 122.116.185.23
※ 文章网址: https://webptt.com/cn.aspx?n=bbs/C_and_CPP/M.1507613289.A.22D.html
4F:推 jerryh001: C语言的特色就是更快,还要更快 额外的动作都浪费时间 10/10 13:38
5F:推 AstralBrain: 那你希望int x = *VACANT; 的x的值是多少 10/10 13:45
6F:→ AstralBrain: 未初始化的垃圾值? 这样跑到後面会出现更多问题 10/10 13:45
7F:→ AstralBrain: 还不如在一开始直接死 10/10 13:45
就跟 NULL 一样 型态都是 void* 只是一种特殊指标
1. void *ptr = VACANT;
*ptr = dont_care; // 没事发生
或是说
2. void val = "i don't care"; // 一样没事
也就是说对 void 赋值是无反应的 www
但是如果是
3. void *ptr = NULL;
*ptr = "segfault" // 错误发生
这个就是按照 lagacy 的做法
8F:→ AstralBrain: 如果等号左边是复杂的c++ class那更无解了 10/10 13:47
C++ 太复杂惹
9F:推 AstralBrain: 所以其实你要的不是VACANT, 而是对void做各种运算..? 10/10 13:53
10F:→ AstralBrain: 那 int *ptr = (int*)VACANT; 会发生什麽事 10/10 13:55
这个应该就真的比较复杂了
假定编译器头文件是这样实作的
#define VACANT ((void*)(0xFFFFFFFFFFFFFFF))
也就是要从 CPU 设计变更下手
只要对存取最後一个保留地址当作没反应的指令
就像 NOP 一样 这样也许可行吧(?
11F:推 AstralBrain: 其实我想问的是你允不允许VACANT转型成其他指标 XD 10/10 14:06
12F:→ AstralBrain: 如果允许的话就回到我一开始的问题 10/10 14:06
二楼的那个问题在於「把 void 型态赋值到 int」
如果改成 int x = *(uintptr_t*)VACANT; 的话...
答案大概就是... x = 0xFFFFFFFF 吧(从 64bit 变成 int: 32bit 截断)
13F:→ AstralBrain: *(int*)VACANT 要segfault还是要NOP 10/10 14:07
14F:→ AstralBrain: segfault => 跟null有87%像, nop => 跑到後面更惨 10/10 14:08
*(int*)VACANT = (int)5487; 的话应该比较简单
毕竟 VACANT 代表的就是最後的保留地址的数值,那转型再取值应该就还是没反应吧
应该是这麽说 VACANT 的数值也是机器依赖的
编译器要端看 CPU 是怎麽实作的
以假设对 CPU 来说地址第 8~15 位元组用作忽略地址的话
那麽 #define VACANT ((void*)(0x8)) 才是正确的
不过这在 CPU 解码前期就要多一个小电路
让该送进来指令变成气泡或是直接让下一个指令直接递补上
这样其实也不会太难
只要 63 个 OR gates 和 1~2 个 NOT gate 去当 mux 的 select
看是是不是要触发 instruction queue 的递补(?
15F:推 AstralBrain: 实作好处理 问题是要怎麽限制vacant只能write-only 10/10 14:23
好问题 @@
如果是
int *ptr = VACANT;
printf("%d\n", *ptr);
的话呢...
应该还是 segfault 跟 NULL 一样
我觉得应该是这样
毕竟 VACANT 的地址不在程序的可以读的权限里
但是所有的程序对那个地址都有写入的权限
跟 /dev/null 一样 只要读就是直接拿到一个 EOF
16F:推 AstralBrain: 现在你要的功能应该可以用c++自己做一个出来 10/10 14:25
17F:→ AstralBrain: 在smart pointer外面再包一层之类的 10/10 14:26
18F:→ AstralBrain: 可以先自己试用看看 XD 10/10 14:26
19F:推 CoNsTaR: 就算这样比较好编译器也做不到啊 10/10 14:27
20F:→ CoNsTaR: 因为很多情况下编译器没办法知道一个变数(当然也包含指 10/10 14:27
21F:→ CoNsTaR: 标变数)的值 10/10 14:27
22F:→ CoNsTaR: 例如假设有个不可判定的问题,它有两种可能的答案(例如是 10/10 14:27
23F:→ CoNsTaR: 、否) 10/10 14:27
24F:→ CoNsTaR: 写一个试图解决这个问题的函式,如果答案是是,那就将指 10/10 14:27
25F:→ CoNsTaR: 标 assign 为 NULL,否则 assign 为其他值 10/10 14:27
26F:→ CoNsTaR: 那如果编译器要知道这个指标的值为何,就必须要先知道这 10/10 14:27
27F:→ CoNsTaR: 个问题的答案 10/10 14:27
28F:→ CoNsTaR: 所以很明显这样的构想是不能成立的 10/10 14:27
没错 正因为如此
所以才用 preprocessor macro
指定一个机器相依的数值
编译器不需要知道 void* 型态的变数是不是装 VACANT
因为 VACANT 就是一个定值
当语言被编译成执行档的时候
会变成 movl $10, 0x8
当机器要执行这行指令的时候
就会直接把 10th register 的值直接丢掉
读取的时候也会
因为程序没有这段地址的读取权限产生 segfault
29F:推 CoNsTaR: 或者说,那个问题的答案是一个整数,然後你把答案转型 as 10/10 14:32
30F:→ CoNsTaR: sign 给指标,那编译器就得知道它的答案是否为零 10/10 14:32
31F:推 LPH66: 你在讲的不就是在编译时期挡下来吗? 10/10 15:50
32F:→ LPH66: 那你又提执行时期的数值做什麽... 10/10 15:50
ㄛㄛ我忘了改 XD
那是最早的讨论
所以後来我们改变了一点实作的方向
现在觉得用软解不太好
用硬体去解应该好得多
33F:→ LPH66: 然後就是因为编译时期挡不下来 (理由如上述) 才会变成 UB 10/10 15:51
34F:推 LPH66: 硬解那就跟 C 语言无关啦, 其他人又不用你的特别硬体 10/10 15:56
没错 一开始觉得靠 compiler 就行了
但是好像问题越来越复杂
如果把这个问题交给 CPU 去处理会简单非常多
35F:→ LPH66: 然後 (虽然离原题离版题都很远) 你或许可以看看 MIPS 10/10 16:02
36F:→ LPH66: 它的 $0 这个 register 是常数零, 读它得零, 写它是 NOP 10/10 16:02
37F:→ LPH66: 虽然这是 register 不是位址但大概跟你想要的东西沾了个边 10/10 16:03
恩恩 $0 是 hard-wired 的
如果要像我说的设计的话就是要写一个小组合电路去控制指令伫列
有点不太一样
38F:推 Lipraxde: 不是应该先设计指令集在用硬体实作吗? 10/10 19:15
39F:推 sppmg: C不是一直被称作中阶语言吗 ^_^ 10/10 19:33
C 语言素有最低阶的高阶语言之称 www
40F:推 chchwy: Objective-C提供了喔 10/10 19:49
41F:推 Bencrie: x86 realmode 就不会喷 segfault 啦 10/10 20:01
42F:→ Bencrie: 但是改 IVT 会发生什麽事就不知道惹 10/10 20:02
43F:推 lc85301: 好了大家别吵了都来写Rust吧(误 10/10 21:17
噗要
44F:→ PkmX: 好奇你的VACANT要怎麽减少sentinel value和减少判断 10/10 22:10
我想要设计一个 circular queue
如果先填入 sentinel value (SV) 的话
那接下来的使用就要去判断
如果是 SV 的话就要略过 这样效率不好
问题卡在 queue 一刚开始运作还没填满的时候
就算用额外的变数储存目前伫列储存了几个元素
使用的时候还是要去判断是不是已经满了
而在我目前的这个情况
刚好不适合宣告两个变数分别指向头跟尾
所以只有一个变数的话 SV 就显得重要
45F:推 AstralBrain: 仔细一想反正你只是要一块write-only的垃圾位址 10/10 22:22
46F:→ AstralBrain: 那 void* vacant = new char[4096]; 就好啦 XD 10/10 22:22
47F:推 AstralBrain: 是不是真的nop也不是很重要 10/10 22:25
没错我後来就是用这个方法 workaround
48F:推 CoNsTaR: 其实 Idris、Agda 的编译器可以做到你想的东西,只是它 10/10 23:35
49F:→ CoNsTaR: 们是和 C 非常不同的语言 10/10 23:35
50F:→ CoNsTaR: 我是觉得 C 应该无法(也不需要)做到这样的事情啦 10/10 23:35
硬体支援 C 语言就能跟上惹(误
51F:→ azureblaze: 没有痛觉神经就不怕被刀砍了 10/11 00:19
52F:→ azureblaze: 程式写错会crash是好事 10/11 00:21
53F:→ azureblaze: debug写入资料遗失听起来就超好玩 10/11 00:26
提早 crash 是好了 至少知道程序是错的
没挂掉有时候不代表是正确的 XD
54F:→ james732: 其实有点好奇其他语言(如rust)怎麽解决这问题 10/11 00:59
55F:→ PkmX: 痾你还是没有解释如何用VACANT避免circular buffer的SV和 10/11 01:26
56F:→ PkmX: 判断啊 有了VACANT不用判断就可以知道buffer满了? 10/11 01:26
是的 因为我的 circular queue 装的是 pointers to type T
所以如果能 pre-filled with VACANT which is of type void*
那麽如此一来我每次要使用的时候就不怕 dereference 到 sentinel 了
就算遇到 sentinel 我也能直接 deref. 而不用怕写至奇怪的地址
因为我处理的这个情况比较特别 要 push 一整圈完之後才开始 pop
如果能等价於在 push 的同时也 pop 前一个那麽 code 会比较 concise
这个时候 VACANT 就很有用 可以让我只需要一个 local variable
57F:→ PkmX: 你一开始里面都塞VACANT那你push的东西到底写到哪里去了? 10/11 01:53
前几次要取的时候会有很多个 VACANT
等到操作越来越多次後
VACANT 就会渐渐被我塞入的值取代直到完全消失为止
就不必再之後的每次回圈里产生 conditional branch 来判断 SV
概念上大概是
int index = -1;
TYPE *queue[64] = {VACANT, ...}; // 64 times
while ( /* CONDITIONS */ )
{
index = (index + 1) % 64; // range 0~63
*(queue[index]) = ...; // write once /* 问题在这
如果没有 VACANT 事情就会比较麻烦 */
queue[index] = ...;
}
58F:推 CoNsTaR: 可是 C 预设是不做 prefill 的啊,这不符合 C 的精神10/11 02:10
59F:→ CoNsTaR: 我觉得你这样又 prefill 拖效能,遇到错误又让他蒙混过去10/11 02:10
60F:→ CoNsTaR: 当作没发生,而且又不直觉,真心觉得不是什麽好办法10/11 02:10
61F:→ CoNsTaR: 现在其他语言处理这种问题不拖效能而且又能在编译时期处10/11 02:10
62F:→ CoNsTaR: 理完的通常都是用 depnedent types 吧…10/11 02:10
这是个好问题
我要操作这个 queue 超过 10兆 次以上
但是 prefill 这个 queue 也不过 64 个 elements
几乎是微乎其微 两权相害取其轻 XD
63F:推 CoNsTaR: 像 rust 也有用的 linear types 可以知道哪些东西存取过10/11 02:15
64F:→ CoNsTaR: 了,哪些还没,和存取次数,而且也是编译时期就检查完,10/11 02:15
65F:→ CoNsTaR: 也是一种方法10/11 02:15
66F:→ PkmX: 你那样写的意思是前面几个iteration *(queue[index]) 因为 10/11 03:00
67F:→ PkmX: pointer 是 VACANT 所以值写进去被 discard 也是合理的? 10/11 03:00
68F:→ Hazukashiine: 嗯嗯 我希望是这样没错 10/11 03:03
69F:→ PkmX: 这样的话 最直觉的方式还是就分配一个垃圾位置当初始值就好 10/11 03:22
70F:→ PkmX: 或是既然你知道最後SV会不见 就分成两个版本 前面的需要检查 10/11 03:23
71F:→ PkmX: 而後面的phase已知SV不存在 就不需要检查 当然比较进阶的 10/11 03:23
72F:→ PkmX: type system可以帮你纪录你的资料结构里面是否还存在SV 10/11 03:24
73F:→ PkmX: (e.g. phantom type) 不过这个已经扯远了 10/11 03:25
74F:→ PkmX: 当然程式语言或是硬体是否要支援这种blackhole的位置我想 10/11 03:26
75F:→ PkmX: 实作上都是没有问题的 只是有没有必要为了这个例子而去复杂 10/11 03:26
76F:→ PkmX: 化语言的spec或是硬体的ISA罢了 10/11 03:26
77F:→ PkmX: 另外多了一个branch在整个loop中+有分支预测的CPU执行下所 10/11 03:28
78F:→ PkmX: 造成的效能影响多寡也是一个要探讨的问题 10/11 03:28
79F:推 kaneson: 不会叫的bug才是最难找的 10/11 13:00
80F:推 dou0228: 同意楼上,忽略是一个最糟糕的写法,不应该这样做 10/11 17:10
81F:→ samuelcdf: 看起来好像只是把programmer的责任丢给compiler一样 10/12 15:36
82F:→ samuelcdf: 就跟很多有自动记忆体配置回收的语言一样, 写的不好, 10/12 15:38
83F:→ samuelcdf: 只是延後整个软体挂掉的时间一样. 而且更难除错. 10/12 15:38
84F:→ uranusjr: 只要忽略的逻辑清楚行为直观就不糟糕, 断言「不应该」是 10/14 02:12
85F:→ uranusjr: 不太好...其实也有很多语言这麽做, 尤其 Smalltalk 派 10/14 02:12
86F:→ uranusjr: 其实原 po 的发想完全很合理, 也是已经被充分讨论的议题 10/14 02:13
87F:→ uranusjr: 只是和 C 类语言发展的方向不同, 所以感觉在这里没温暖 10/14 02:14
88F:→ uranusjr: 结论是快转换阵营吧 C-like languages 不是一切 (欸 10/14 02:14
※ 编辑: Hazukashiine (122.116.185.23), 10/14/2017 11:40:43
89F:推 Killercat: 这东西我在Obj-C被婊了无数次,这不是一个好方法... 10/14 21:55
90F:→ Killercat: Obj-C你对任何nil(相当於nullptr)的操作都会无声的过去 10/14 21:56
91F:→ Killercat: 应该说,这对大多数C/C++ user来讲 是很不习惯的事情 10/14 21:56
92F:→ Killercat: 另外除错困难等级里面 pre-compiler < compiler < run 10/14 21:58
93F:→ Killercat: time <<<<<<< silently pass 这刚好属於最糟的一种 10/14 21:58
94F:→ LiloHuang: 推 Killercat 的见解,没有 segfault 无声无息很糟糕.. 10/15 00:41
95F:推 steve1012: 不会报错的错是最惨的 几乎没好处 10/15 06:47
96F:→ y3k: 这个设计应该有某部分人会需要 感觉也可行 但是对正常人来说 10/15 11:25
97F:→ y3k: B>Z 原因很简单就是上面说的不会叫的bug最难找 这种设计下去 10/15 11:26
98F:→ y3k: 往往只是逼Programer更频繁的check null或你说的虚无... 10/15 11:27
99F:→ y3k: 而且你要知道正常的C/C++ Code规模XD 10/15 11:28
100F:→ y3k: 所以如果要做 应该是可以做 但是绝对不会预设为启用 这很糟 10/15 11:28