Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[UE] Bug: TS对UE原生容器for of迭代的异常表现?似乎是局部变量被提前GC了 #1776

Open
3 tasks done
slowmna opened this issue Jun 28, 2024 · 11 comments
Open
3 tasks done
Assignees
Labels
bug Something isn't working Unreal

Comments

@slowmna
Copy link

slowmna commented Jun 28, 2024

前置阅读 | Pre-reading

Puer的版本 | Puer Version

1.0.6

UE的版本 | UE Version

5.3

发生在哪个平台 | Platform

Editor(win)

错误信息 | Error Message

image
TS代码里,Obj是一个U对象,TestA,TestB是UStruct

UCLASS()
class UDummyObj : public UObject
{
GENERATED_BODY()

public:
int num;
};

USTRUCT(BlueprintType)
struct FTestB
{
GENERATED_USTRUCT_BODY()
public:

FTestB() {}

FTestB(const TArray<TObjectPtr<UDummyObj>>& InTasks): MyDummy(InTasks) {}

UPROPERTY(BlueprintReadWrite, EditAnywhere, Instanced)
TArray<TObjectPtr<UDummyObj>> MyDummy;

};

USTRUCT(BlueprintType)
struct FTestA
{
GENERATED_USTRUCT_BODY()
public:
UPROPERTY(BlueprintReadWrite, EditAnywhere)
TMap<int, FTestB> MyTestB;
};

UCLASS()
class UTestOuter : public UObject
{
GENERATED_BODY()

public:

UFUNCTION()
void DoSomething();

UFUNCTION()
const FTestA& ReturnConstRef();


FTestA MyTestA;

};

问题重现 | Bug reproduce

https://github.com/slowmna/puerts_unreal_demo
已经提交了复现工程,直接运行放着跑,我这里几分钟之后第一次打印出console.errorr
image
从图上可以看到TestA的地址被释放了然后才for of遍历的

大概10分钟后就频繁出现,几乎稳定必现:
image

@slowmna slowmna added bug Something isn't working Unreal labels Jun 28, 2024
@chexiongsheng
Copy link
Collaborator

应该是v8的认为后续代码用不上StructA了,于是释放了。
你试试在循环之后加一行代码访问下StructA。

@slowmna
Copy link
Author

slowmna commented Jun 28, 2024

应该是v8的认为后续代码用不上StructA了,于是释放了。 你试试在循环之后加一行代码访问下StructA。

是的,我们工程里最后加了一行打印StructA的Num后就不会出现此问题了,这算不算是v8的BUG?

@chexiongsheng
Copy link
Collaborator

chexiongsheng commented Jun 28, 2024

应该是v8的认为后续代码用不上StructA了,于是释放了。 你试试在循环之后加一行代码访问下StructA。

是的,我们工程里最后加了一行打印StructA的Num后就不会出现此问题了,这算不算是v8的BUG?

这是v8的gc算法先进性/或者是编译优化做的好的体现。
可能你代入了你以往接触到的gc的印象才认为是bug。
puerts的JsEnv有个宏WITH_OUTER_LINK,加了后会增加内层节点对外层节点引用,能解决这种疏忽问题,但会增加内存。

@chexiongsheng
Copy link
Collaborator

chexiongsheng commented Jun 28, 2024

应该是v8的认为后续代码用不上StructA了,于是释放了。 你试试在循环之后加一行代码访问下StructA。

是的,我们工程里最后加了一行打印StructA的Num后就不会出现此问题了,这算不算是v8的BUG?

其实按v8运行的实例个数和时长来说,和操作系统核心模块是一个级别的。有问题动不动怀疑是v8的bug有点盲目自信了。

@slowmna
Copy link
Author

slowmna commented Jun 28, 2024

应该是v8的认为后续代码用不上StructA了,于是释放了。 你试试在循环之后加一行代码访问下StructA。

是的,我们工程里最后加了一行打印StructA的Num后就不会出现此问题了,这算不算是v8的BUG?

其实按v8运行的实例和时长来说,和操作性核心模块是一个级别的。有问题动不动怀疑是v8的bug有点盲目自信了。

我懂你意思了,也就是for of因为是迭代的目标是StructA.MyTestB, 而不是StructA本身, 而JavaScript本身是可以单独引用MyTestB而释放StructA的,这种情况下激进的GC策略也不会导致BUG。但仍然是一个危险的优化,这个GC的行为越过了通常意义上理解的生命周期, 我特地搜索了ECMAScript6规范,let 变量的作用域是块级作用域,v8错误的估计了从JavaScript的代码上没有访问,于是在块结束前就提前GC了。照这么说来。 WITH_OUTER_LINK这个宏就不是可选项,而是必选项。这不是我们的疏忽,是v8的激进策略和宿主语言的不搭配导致的。 不可能,也不应该有人预先假设块级作用域在退出{}之前就释放。如果v8没有提供任何规避此问题的手段的话,我只能说这玩意就是给纯JavaScript环境用的吧,和宿主语言的跨语言调用现在这样子能用?

按照我的理解来说,如果不加这个宏,下面的语句也可能崩溃:
image
因为从获取到StructA之后就没有直接对StructA的访问了,后续用的都是MyTestB,按照这个情况来看,基本上用到UStruct
的地方,几乎都可以明明白白非常正当的崩溃

@chexiongsheng
Copy link
Collaborator

chexiongsheng commented Jun 28, 2024

我觉得你是把c++的scope的一些对象声明周期管理给拿到其它语言了。
scope只有变量可见性是各语言通用的。es规范也是指这个。
scope结束释放对象是c++这种不带gc语言的特殊设定。
对于带gc的语言,gc何时释放对象显然和scope没关,即不保证scope结束对象必须gc完成,也不保证scope前不得释放对象,或者你在es规范找下这方面的定义?c++的scope结束释放对象是在c++标准里的。

@slowmna
Copy link
Author

slowmna commented Jun 28, 2024

我觉得你是把c++的scope的一些对象声明周期管理给拿到其它语言了。 scope只有变量可见性是各语言通用的。es规范也是指这个。 scope结束释放对象是c++这种不带gc语言的特殊设定。 对于带gc的语言,gc何时释放对象显然和scope没关,即不保证scope结束对象必须gc完成,也不保证scope前不得释放对象,或者你在es规范找下这方面的定义?c++的scope结束释放对象是在c++标准里的。

行吧,一般人都容易想到 GC生命周期 >= 可见性,没见过 GC生命周期 < 可见性 的,
那对于这样的问题怎么办?只有把WITH_OUTER_LINK改成默认打开了吧
image

@chexiongsheng
Copy link
Collaborator

你这种情况反而比较少见,所以WITH_OUTER_LINK也不是专门解决这种问题。
WITH_OUTER_LINK解决的是这些更容易犯的错误:
foo(StrunctA.MyTestB);//然后foo把参数存起来用

let test = StrunctA.MyTestB
botton.onClick = () = {
test.xxx;
}

有个团队(有比较多的外包)如上错误比较多,所以我加了WITH_OUTER_LINK,但我不知道是不是普遍现象。

@slowmna
Copy link
Author

slowmna commented Jun 28, 2024

你这种情况反而比较少见,所以WITH_OUTER_LINK也不是专门解决这种问题。 WITH_OUTER_LINK解决的是这些更容易犯的错误: foo(StrunctA.MyTestB);//然后foo把参数存起来用

let test = StrunctA.MyTestB botton.onClick = () = { test.xxx; }

有个团队(有比较多的外包)如上错误比较多,所以我加了WITH_OUTER_LINK,但我不知道是不是普遍现象。

你说的这个问题我们考虑到了,所以我们才觉得只要我持有最外层的Struct的引用,内层的的值就不会释放,现在看起来并不能,最外层的值在v8看起来没有再用了就可能会释放。我猜其他团队也许是恰好最外层还在用,而且本身这个复现的几率就很小。
但对于我举例的三行代码,除了这个宏和v8不要在可见性内GC两种手段之外,没有什么很好的解决方案。我打算打开这个宏算了。

@chexiongsheng
Copy link
Collaborator

你这种情况反而比较少见,所以WITH_OUTER_LINK也不是专门解决这种问题。 WITH_OUTER_LINK解决的是这些更容易犯的错误: foo(StrunctA.MyTestB);//然后foo把参数存起来用
let test = StrunctA.MyTestB botton.onClick = () = { test.xxx; }
有个团队(有比较多的外包)如上错误比较多,所以我加了WITH_OUTER_LINK,但我不知道是不是普遍现象。

你说的这个问题我们考虑到了,所以我们才觉得只要我持有最外层的Struct的引用,内层的的值就不会释放,现在看起来并不能,最外层的值在v8看起来没有再用了就可能会释放。我猜其他团队也许是恰好最外层还在用,而且本身这个复现的几率就很小。 但对于我举例的三行代码,除了这个宏和v8不要在可见性内GC两种手段之外,没有什么很好的解决方案。我打算打开这个宏算了。

其实你的理解还是有些问题:你的理解是语言语法描述有个变量A,于是真实代码编译运行后,还是得有个变量A对应的某个实体。

但实际上变量,可见性都是语言层面的东西,真正的程序/虚拟机中是不必要有的,甚至编译阶段就优化掉。当然也有虚拟机实现有这么个变量实体,比如lua虚拟机会有个upvalue数组。

@slowmna
Copy link
Author

slowmna commented Jun 28, 2024

你这么说也没错,我是没想到这个脚本语言都能优化到这样,而且for of这里不算是引用StructA也很隐蔽(相当于有个this = StructA.MyTestB ?所以第二次循环就不会在用到StructA了)
要是说C++ 定义了一个变量,没有显式使用,然后通过栈顶指针推算这个变量的地址去访问。结果这个变量其实被优化没了导致访问出错很多人还是能想到的

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working Unreal
Projects
None yet
Development

No branches or pull requests

2 participants