DOTween 은 트윈을 만들 때만 할당한다
Esc

    DOTween 은 트윈을 만들 때만 할당한다

    2026년 10월 18일 수정–Unity라이브러리트윈GC
    void OnClick(Vector3 dest)
    {
        transform.DOMove(dest, 0.3f).SetEase(Ease.OutQuad);
    }

    “DOTween 은 트윈을 풀링하니까 할당이 없다” 는 말을 믿고 이벤트마다 트윈을 만들면, 프로파일러의 GC Alloc 열에 클릭할 때마다 수백 바이트가 찍힙니다.

    DOTween 의 비용은 만들 때 할당이 몰리고, 도는 동안에는 할당이 없는 모양입니다. 재활용은 기본으로 꺼져 있고, 켜도 람다 몫은 매번 생깁니다. 기준은 DOTween 1.3.030(GitHub 커밋 4846808)이고, 할당은 ProfilerRecorder 의 “GC Allocated In Frame” 으로 쟀습니다.

    DOMove 한 번은 힙 객체 넷을 만든다

    DOMove 는 target 을 붙잡는 두 람다를 DOTween.To 에 넘깁니다. 컴파일러는 붙잡힌 변수를 담은 캡처 객체 하나를 두 람다가 함께 쓰게 만들고, 거기에 델리게이트 둘과 트윈 객체가 더해집니다. 설치된 DLL 의 DOMove IL 에도 newobj 가 셋(캡처 클래스, DOGetter<Vector3>, DOSetter<Vector3>)입니다. 트윈 객체는 TweenManager.GetTweener 에서 갈립니다.

    // ShortcutExtensions.DOMove
    // 람다 둘이 target 을 붙잡는다. 캡처 객체 1개, DOGetter 1개, DOSetter 1개가 여기서 생긴다
    var t = DOTween.To(() => target.position, x => target.position = x, endValue, duration);
    
    // DOTween.ApplyTo: 트윈 객체를 매니저에게 청한다
    var tweener = TweenManager.GetTweener<T1, T2, TPlugOptions>();
    
    // TweenManager.GetTweener
    if (totPooledTweeners > 0)                       // 재활용이 꺼져 있으면 풀은 늘 비어 있다
    {
        for (int i = _maxPooledTweenerId; i > _minPooledTweenerId - 1; --i)
        {
            Tween tween = _pooledTweeners[i];
    
            // 세 타입이 모두 같은 트윈을 찾으면 그것을 다시 쓴다
            if (tween != null && tween.typeofT1 == typeofT1 && tween.typeofT2 == typeofT2
                && tween.typeofTPlugOptions == typeofTPlugOptions)
            {
                t = (TweenerCore<T1, T2, TPlugOptions>)tween;
                AddActiveTween(t);
                _pooledTweeners[i] = null;
                totPooledTweeners--;
                return t;                            // new 없음
            }
        }
    }
    
    t = new TweenerCore<T1, T2, TPlugOptions>();     // 풀에 없으면 새로 만든다
    totTweeners++;
    AddActiveTween(t);
    return t;
    
    // 다시 DOTween.ApplyTo: 델리게이트를 필드에 넣고 기본값을 채운다. 새로 만드는 것은 없다
    bool setupSuccessful = Tweener.Setup(tweener, getter, setter, endValue, duration, plugin);
    
    // 다시 DOMove
    t.SetOptions(snapping).SetTarget(target);
    람다 쪽 트윈 객체 새 힙 객체
    재활용 끔 (기본) 캡처, getter, setter 3개 새로 만듦 4개 (656 B)
    재활용 켬, 풀에 같은 타입 캡처, getter, setter 3개 풀에서 꺼냄 3개 (280 B)

    재활용을 켜도 람다는 호출마다 새로 만듭니다. 줄어드는 것은 트윈 객체 하나입니다.

    한 프레임에 트윈 100개를 만들어 한 개당으로 나눈 값입니다.

    경로 에디터 배치모드 Mono 플레이어 IL2CPP 플레이어
    DOMove, 재활용 OFF (기본) 656 B 656 B 656 B
    DOMove, 재활용 ON, 풀이 찬 둘째 회 280 B 280 B 280 B

    차이 376 B 가 트윈 객체이고, 280 B 는 재활용을 켜도 남는 캡처 객체와 델리게이트 둘입니다. 세 조건이 바이트 단위로 같아서, 이 크기는 백엔드가 아니라 객체 구성에서 나옵니다. 손으로 쓴 Update 보간은 필드에 값을 넣을 뿐 시작할 때 아무것도 만들지 않고, 코루틴은 시작할 때마다 이터레이터 객체를 만듭니다.

    도는 동안에는 할당하지 않는다

    매 프레임 경로(갱신 루프 → 위치 계산 → 플러그인 → setter)에는 new 가 없습니다. 값은 제네릭 필드와 인자로 흐르고, 콜백은 이미 있는 델리게이트를 부릅니다. 플레이어에서 DOMove 10,000개가 도는 프레임의 할당 중앙값이 트윈 없는 프레임과 같은 686 B(Mono), 722 B(IL2CPP)였습니다. 트윈이 더한 몫은 0 B 입니다.

    프레임 시간은 트윈 수에 비례합니다(서로 다른 Transform 에 건 DOMove, PlayerLoop 중앙값의 기준선 대비 증가분).

    동시 트윈 수 Mono 플레이어 트윈 하나당 IL2CPP 플레이어 트윈 하나당
    1,000 +0.194 ms 194 ns +0.100 ms 100 ns
    10,000 +1.990 ms 199 ns +1.384 ms 138 ns

    에디터 배치모드에서도 10,000개에서 트윈당 178 ns 였습니다. 할당이 문제가 되는 것은 트윈이 많을 때가 아니라 트윈을 자주 만들 때입니다.

    트윈 하나를 들고 다시 쓰면 0 B 다

    재활용을 켜도 람다 몫이 남으니, 할당을 없애는 길은 만드는 호출 자체를 줄이는 것입니다. 되풀이되는 연출은 트윈 하나를 살려 두고 목표만 바꿉니다.

    TweenerCore<Vector3, Vector3, VectorOptions> _move;   // Tweener 가 아니라 TweenerCore 로 든다
    
    void Awake()
    {
        _move = transform.DOMove(transform.position, 0.3f)
            .SetEase(Ease.OutQuad).SetAutoKill(false).SetLink(gameObject).Pause();
    }
    
    public void MoveTo(Vector3 dest)
    {
        _move.ChangeEndValue(dest, true);   // 소스가 "NO-GC METHOD" 라고 적은 제네릭 오버로드
        _move.Restart();
    }

    세 방식을 100회씩 되풀이한 1회당 할당은 656 B, 280 B, 0 B 였습니다. 필드를 Tweener 로 선언하면 ChangeEndValue(object ...) 가 골라져 Vector3 박싱으로 28 B 가 생겼습니다. 트윈을 들고 있을 수 없는 자리에서는 델리게이트 둘을 Awake 에서 한 번 만들어 DOTween.To(_get, _set, dest, 0.3f).SetRecyclable(true) 로 넘기면, 풀이 찬 상태에서 1회당 0.2 B(Mono), 0.0 B(IL2CPP)였습니다.

    주의할 점

    1. “DOTween 은 풀링한다” 를 믿고 할당을 재지 않고 넘어가지 않는다

    안 지키면 생기는 문제. 재활용은 기본으로 꺼져 있어서(DOTween.defaultRecyclable = false) 설정을 건드리지 않은 프로젝트에서는 DOMove 한 번이 656 B 입니다. 켜도 280 B 가 남습니다. 이벤트마다 트윈을 만드는 코드가 자주 불리는 자리에서 GC 가 치울 거리를 계속 쌓습니다.

    이렇게 쓴다. 프로파일러의 GC Alloc 열에서 트윈을 만드는 프레임을 직접 봅니다. 자주 불리는 연출은 트윈 하나를 SetAutoKill(false) 로 살려 두고 ChangeEndValue 와 Restart 로 다시 씁니다.

    2. 다시 쓸 트윈 참조를 Tweener 로 선언하지 않는다

    안 지키면 생기는 문제. Tweener 에는 AOT 때문에 object 를 받는 ChangeEndValue 만 보여서, 값 타입 목표를 넘길 때마다 박싱이 일어납니다. 할당을 없애려고 트윈을 재사용했는데 여기서 1회당 28 B 가 다시 생깁니다.

    이렇게 쓴다. 필드를 숏컷의 반환 타입 그대로 TweenerCore<Vector3, Vector3, VectorOptions> 로 선언하거나 var 로 받습니다. 제네릭 ChangeEndValue(T2 ...) 가 골라집니다.

    3. 매 프레임 트윈을 새로 만들지 않는다

    안 지키면 생기는 문제. 마우스를 따라가게 하려고 Update 안에서 DOMove 를 부르면 프레임마다 트윈 객체와 람다를 만들고, 앞 트윈과 새 트윈이 같은 위치를 두고 겹칩니다. 60 fps 면 초당 약 39 KB 가 쌓이고 동시 트윈 수가 늘어납니다.

    이렇게 쓴다. 목표가 매 프레임 바뀌는 추적은 손으로 쓴 Update 보간(Vector3.MoveTowards, SmoothDamp)으로 합니다. 트윈을 꼭 써야 하면 하나를 들고 ChangeEndValue(target, true) 로 목표만 바꿉니다.

    정리

    DOTween 은 트윈을 만들 때만 할당하고, 도는 동안에는 할당하지 않습니다.

    • DOMove 한 번은 캡처 객체, 델리게이트 둘, 트윈 객체로 656 B 입니다. 재활용은 기본 꺼짐이고, 켜도 람다 몫 280 B 는 남습니다.
    • 도는 동안의 할당은 0 B 이고, 프레임 시간은 트윈 수에 비례해 트윈당 약 100~200 ns 입니다.
    • 트윈 하나를 TweenerCore 로 들고 ChangeEndValue 와 Restart 로 다시 쓰면 1회당 0 B 입니다.