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

Avoid creating most objects that Godot is going to use placement new to initialize (mark 2) #1379

Merged
merged 1 commit into from
Mar 14, 2024

Conversation

dsnopek
Copy link
Collaborator

@dsnopek dsnopek commented Feb 1, 2024

This a new idea for a way to fix issue #1240

It's a less hacky alternative to PR #1378

Rather than messing around with blobs of memory, and trying to cleverly avoid calling object constructors, this just adds a new private constructor that does what we want. :-)

The code is generating for all the variant types, but I've only updated Array and Dictionary to use the new constructors.

@dsnopek dsnopek added the bug This has been identified as a bug label Feb 1, 2024
@dsnopek dsnopek added this to the 4.3 milestone Feb 1, 2024
@dsnopek dsnopek requested a review from a team as a code owner February 1, 2024 20:25
@dsnopek dsnopek force-pushed the uninitialized-value-mk2 branch from 6870ac2 to f22dcc8 Compare February 1, 2024 20:27
@allenwp
Copy link
Contributor

allenwp commented Feb 1, 2024

I think the array is being corrupted with the current state of this PR... Here's my test code:

void GDExample::test_memory_behaviour() {
	UtilityFunctions::print("GDExtension::test_memory_behaviour()...");
	{
		Array original_array;
		original_array.push_back(123);
		{
			Variant my_variant = original_array;
			{
				Array my_array = my_variant;
				int value = my_array[0]; // This line causes an exception.
				UtilityFunctions::print("value: ", value);
			}
		}
	}
	UtilityFunctions::print("GDExtension::test_memory_behaviour() COMPLETE");
}

void GDExample::test_memory_behaviour_multiple() {
	for (int i = 0; i < 1000; i++) {
		test_memory_behaviour();
	}
}

With that, I get this exception when it hits the int value = my_array[0]; line:

Exception thrown: read access violation.
this was nullptr.
 	godot.windows.editor.dev.x86_64.exe!Variant::operator __int64() Line 1457	C++
>	godot.windows.editor.dev.x86_64.exe!VariantTypeConstructor<__int64>::type_from_variant(void * r_value, void * p_variant) Line 1544	C++
 	libgdexample.windows.template_debug.dev.x86_64.dll!godot::Variant::operator __int64() Line 264	C++
 	libgdexample.windows.template_debug.dev.x86_64.dll!godot::Variant::operator int() Line 269	C++
 	libgdexample.windows.template_debug.dev.x86_64.dll!godot::GDExample::test_memory_behaviour() Line 27	C++
 	libgdexample.windows.template_debug.dev.x86_64.dll!godot::GDExample::test_memory_behaviour_multiple() Line 38	C++
 	libgdexample.windows.template_debug.dev.x86_64.dll!godot::call_with_variant_args_helper<godot::GDExample>(godot::GDExample * p_instance, void(godot::GDExample::*)() p_method, const godot::Variant * * p_args, GDExtensionCallError & r_error, IndexSequence<> __formal) Line 242	C++
 	libgdexample.windows.template_debug.dev.x86_64.dll!godot::call_with_variant_args_dv<godot::GDExample>(godot::GDExample * p_instance, void(godot::GDExample::*)() p_method, const void * const * p_args, int p_argcount, GDExtensionCallError & r_error, const std::vector<godot::Variant,std::allocator<godot::Variant>> & default_values) Line 366	C++
 	libgdexample.windows.template_debug.dev.x86_64.dll!godot::MethodBindT<godot::GDExample>::call(void * p_instance, const void * const * p_args, __int64 p_argument_count, GDExtensionCallError & r_error) Line 325	C++
 	libgdexample.windows.template_debug.dev.x86_64.dll!godot::MethodBind::bind_call(void * p_method_userdata, void * p_instance, const void * const * p_args, __int64 p_argument_count, void * r_return, GDExtensionCallError * r_error) Line 99	C++
 	godot.windows.editor.dev.x86_64.exe!GDExtensionMethodBind::call(Object * p_object, const Variant * * p_args, int p_arg_count, Callable::CallError & r_error) Line 213	C++
 	godot.windows.editor.dev.x86_64.exe!Object::callp(const StringName & p_method, const Variant * * p_args, int p_argcount, Callable::CallError & r_error) Line 775	C++
 	godot.windows.editor.dev.x86_64.exe!Callable::callp(const Variant * * p_arguments, int p_argcount, Variant & r_return_value, Callable::CallError & r_call_error) Line 68	C++
 	godot.windows.editor.dev.x86_64.exe!Object::emit_signalp(const StringName & p_name, const Variant * * p_args, int p_argcount) Line 1128	C++
 	godot.windows.editor.dev.x86_64.exe!Node::emit_signalp(const StringName & p_name, const Variant * * p_args, int p_argcount) Line 3673	C++
 	godot.windows.editor.dev.x86_64.exe!Object::emit_signal<>(const StringName & p_name) Line 922	C++
 	godot.windows.editor.dev.x86_64.exe!BaseButton::_pressed() Line 139	C++
 	godot.windows.editor.dev.x86_64.exe!BaseButton::on_action_event(Ref<InputEvent> p_event) Line 177	C++
 	godot.windows.editor.dev.x86_64.exe!BaseButton::gui_input(const Ref<InputEvent> & p_event) Line 70	C++
 	godot.windows.editor.dev.x86_64.exe!Control::_call_gui_input(const Ref<InputEvent> & p_event) Line 1798	C++
 	godot.windows.editor.dev.x86_64.exe!Viewport::_gui_call_input(Control * p_control, const Ref<InputEvent> & p_input) Line 1603	C++
 	godot.windows.editor.dev.x86_64.exe!Viewport::_gui_input_event(Ref<InputEvent> p_event) Line 1869	C++
 	godot.windows.editor.dev.x86_64.exe!Viewport::push_input(const Ref<InputEvent> & p_event, bool p_local_coords) Line 3369	C++
 	godot.windows.editor.dev.x86_64.exe!Window::_window_input(const Ref<InputEvent> & p_ev) Line 1616	C++
 	godot.windows.editor.dev.x86_64.exe!call_with_variant_args_helper<Window,Ref<InputEvent> const &,0>(Window * p_instance, void(Window::*)(const Ref<InputEvent> &) p_method, const Variant * * p_args, Callable::CallError & r_error, IndexSequence<0> __formal) Line 304	C++
 	godot.windows.editor.dev.x86_64.exe!call_with_variant_args<Window,Ref<InputEvent> const &>(Window * p_instance, void(Window::*)(const Ref<InputEvent> &) p_method, const Variant * * p_args, int p_argcount, Callable::CallError & r_error) Line 419	C++
 	godot.windows.editor.dev.x86_64.exe!CallableCustomMethodPointer<Window,Ref<InputEvent> const &>::call(const Variant * * p_arguments, int p_argcount, Variant & r_return_value, Callable::CallError & r_call_error) Line 99	C++
 	godot.windows.editor.dev.x86_64.exe!Callable::callp(const Variant * * p_arguments, int p_argcount, Variant & r_return_value, Callable::CallError & r_call_error) Line 57	C++
 	godot.windows.editor.dev.x86_64.exe!Callable::call<Ref<InputEvent>>(Ref<InputEvent> <p_args_0>) Line 864	C++
 	godot.windows.editor.dev.x86_64.exe!DisplayServerWindows::_dispatch_input_event(const Ref<InputEvent> & p_event) Line 2961	C++
 	godot.windows.editor.dev.x86_64.exe!DisplayServerWindows::_dispatch_input_events(const Ref<InputEvent> & p_event) Line 2931	C++
 	godot.windows.editor.dev.x86_64.exe!Input::_parse_input_event_impl(const Ref<InputEvent> & p_event, bool p_is_emulated) Line 760	C++
 	godot.windows.editor.dev.x86_64.exe!Input::flush_buffered_events() Line 1026	C++
 	godot.windows.editor.dev.x86_64.exe!DisplayServerWindows::process_events() Line 2647	C++
 	godot.windows.editor.dev.x86_64.exe!OS_Windows::run() Line 1476	C++
 	godot.windows.editor.dev.x86_64.exe!widechar_main(int argc, wchar_t * * argv) Line 182	C++
 	godot.windows.editor.dev.x86_64.exe!_main() Line 204	C++
 	godot.windows.editor.dev.x86_64.exe!main(int argc, char * * argv) Line 218	C++
 	godot.windows.editor.dev.x86_64.exe!WinMain(HINSTANCE__ * hInstance, HINSTANCE__ * hPrevInstance, char * lpCmdLine, int nCmdShow) Line 232	C++

@dsnopek dsnopek force-pushed the uninitialized-value-mk2 branch from f22dcc8 to 7d82c48 Compare February 1, 2024 21:22
@dsnopek
Copy link
Collaborator Author

dsnopek commented Feb 1, 2024

Ack, I made a really dumb mistake (fixed in my last push)! Sorry for wasting your effort on testing that. I'll do some testing of my own with your test code.

@dsnopek
Copy link
Collaborator Author

dsnopek commented Feb 2, 2024

@allenwp I just tested with the code you shared above, and in my latest version I'm not seeing the corruption or the memory leak! When I have a chance, I'm going to expand this PR to address all Variant types and take it out of draft.

@allenwp
Copy link
Contributor

allenwp commented Feb 2, 2024

Oh, yeah, at first glance, this looks 1000x better than the other PR. Nicely done! Thanks!

I've tried testing this with my boiled down minimum reproduction code as well as my personal project where I first noticed the leak: Both of these were fixed by this PR draft!

@dsnopek dsnopek marked this pull request as draft February 2, 2024 21:51
@dsnopek dsnopek marked this pull request as ready for review February 21, 2024 13:52
@dsnopek dsnopek changed the title [DRAFT] Avoid creating objects that Godot is going to use placement new to initialize (mark 2) Avoid creating objects that Godot is going to use placement new to initialize (mark 2) Feb 21, 2024
@dsnopek dsnopek changed the title Avoid creating objects that Godot is going to use placement new to initialize (mark 2) [DRAFT] Avoid creating objects that Godot is going to use placement new to initialize (mark 2) Feb 21, 2024
@dsnopek dsnopek marked this pull request as draft February 21, 2024 13:53
@cyberpuffin-digital
Copy link

cyberpuffin-digital commented Feb 26, 2024

I applied these changes to my project, and the memory leak disappeared. My benchmark, which usually leaks ~4 GB / min, shows a steady 183MiB static memory throughout.

@dsnopek dsnopek force-pushed the uninitialized-value-mk2 branch 2 times, most recently from 4ff8785 to 50f7123 Compare February 26, 2024 16:04
@dsnopek dsnopek changed the title [DRAFT] Avoid creating objects that Godot is going to use placement new to initialize (mark 2) Avoid creating objects that Godot is going to use placement new to initialize (mark 2) Feb 26, 2024
@dsnopek dsnopek marked this pull request as ready for review February 26, 2024 16:04
@dsnopek
Copy link
Collaborator Author

dsnopek commented Feb 26, 2024

@cyberpuffin-digital Thanks for the review and testing!

I've updated this PR to cover most variant types: definitely all the ones that use reference counting and could leak, plus a couple others which were easy to do. So, I've taken this PR out of draft!

There's a few types where we could avoid initializing them to their defaults, such as Vector2/3/i and Transform2D/3D, but those classes are hand-written and don't call into Godot for anything else, so I've left them as-is for now. We can come back and make that slight optimization later.

@allenwp
Copy link
Contributor

allenwp commented Feb 26, 2024

There's a few types where we could avoid initializing them to their defaults, such as Vector2/3/i and Transform2D/3D, but those classes are hand-written and don't call into Godot for anything else, so I've left them as-is for now. We can come back and make that slight optimization later.

Maybe add a comment describing this? If any future issues come up, it would be good to easily see that there is no strong intention for the difference between types that use the new generated constructor vs. the "old" way of creating a temporary variable and calling to_type_constructor.

@dsnopek dsnopek force-pushed the uninitialized-value-mk2 branch from 50f7123 to c4fde85 Compare February 26, 2024 16:36
@dsnopek
Copy link
Collaborator Author

dsnopek commented Feb 26, 2024

Maybe add a comment describing this?

Makes sense! I've added a @todo comment in the conversion methods where we could consider avoiding the extra initialization in the future.

@dsnopek dsnopek changed the title Avoid creating objects that Godot is going to use placement new to initialize (mark 2) Avoid creating most objects that Godot is going to use placement new to initialize (mark 2) Feb 26, 2024
@allenwp
Copy link
Contributor

allenwp commented Feb 26, 2024

Makes sense! I've added a @todo comment in the conversion methods where we could consider avoiding the extra initialization in the future.

Thanks! That leaves only the following untouched, which all have slight differences compared to the other operators which have now been converted or had the @todo added:

bool
int64_t
double
Object *
ObjectID

@dsnopek
Copy link
Collaborator Author

dsnopek commented Feb 26, 2024

Yep, those ones should stay the way they are, because they are already not initializing their values.

@allenwp
Copy link
Contributor

allenwp commented Mar 12, 2024

I've been using this PR in my fork of godot-cpp for a week or so and haven't discovered any instabilities with it.

@@ -145,6 +143,7 @@ class Variant {
static GDExtensionTypeFromVariantConstructorFunc to_type_constructor[VARIANT_MAX];

public:
_FORCE_INLINE_ GDExtensionVariantPtr _native_ptr() const { return const_cast<uint8_t(*)[GODOT_CPP_VARIANT_SIZE]>(&opaque); }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we keep the underscore here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the other variant types have a public _native_ptr() method, so this would match them. For some reason this one was private instead of public. In any case, it's not really part of the public API (since this method isn't in Godot), but we need these to be public for technical reasons.

}

Variant::operator Vector2() const {
// @todo Avoid initializing result before calling constructor (which will initialize it again)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible?

Copy link
Collaborator Author

@dsnopek dsnopek Mar 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is possible, but we decided to postpone it to later.

This PR fixes the memory leak. Avoiding initializing those other results would be a slight performance improvement, but those aren't leaking memory, so fixing them isn't as urgent. The @todo's are there to remind us to fix it eventually, though :-)

Copy link
Member

@adamscott adamscott left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code seems alright! I only compiled it, didn't try it out, but @allenwp seems to have tested it quite thoroughly.

@dsnopek dsnopek merged commit a62f633 into godotengine:master Mar 14, 2024
12 checks passed
@dsnopek
Copy link
Collaborator Author

dsnopek commented Mar 14, 2024

Thanks!

@PiCode9560
Copy link

Is this somehow also fixed for other branches/tags, or is it just master?

@dsnopek
Copy link
Collaborator Author

dsnopek commented Apr 6, 2024

This is marked to be cherry-picked to 4.2 and 4.1, but it hasn't been done yet

@dsnopek
Copy link
Collaborator Author

dsnopek commented Apr 8, 2024

Cherry-picked for 4.1 in PR #1411

@dsnopek
Copy link
Collaborator Author

dsnopek commented Apr 8, 2024

Cherry-picked for 4.2 in PR #1410

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug This has been identified as a bug
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants