개발/공부

[유니티] for과 foreach 성능 비교

메피카타츠 2023. 7. 24. 23:58

(Unity 2021.3.10f1 기준)

 

+2024.03.30.

List는 가비지를 생성하지 않는 것이 맞고, Dictionary는 처음에만 조금 가비지를 생성하는 것? 같다. 자료 구조에 따라 가비지가 생성될 수도 있으니 애매하면 프로파일러를 사용해서 확인해보면 좋을 것 같다.

JacksonDunstan.com | Do Foreach Loops Still Create Garbage?

 

JacksonDunstan.com | Do Foreach Loops Still Create Garbage?

Over a year ago I wrote an article title Do Foreach Loops Create Garbage using Unity 5.2 and tested foreach with a variety of collections: List, Dictionary, arrays, etc. Since then Unity has a new C# compiler and version 5.6 has been released. Is it safe t

www.jacksondunstan.com

 

[결론]

1. foreach는 더 이상 가비지를 생성하지 않는 것이 맞다. (+2024.03.30. List와 Dictionary에서)

2. 배열이나 List와 같이 index로 접근 가능한 경우, for문을 사용하는 게 속도도 50~60% 가량 빠르고 데이터를 제어하기도 편리하니 for문을 사용하는 것이 좋다.

3. Dictionary의 key로 된 List를 만들어서 for문을 돌리는 것보다도 foreach로 Dictionary를 돌리는 것이 2배 가량 빠르고 가비지가 안 나오니 Dictionary를 탐색할 때는 걱정없이 foreach를 사용하자.

 

for와 foreach의 성능 차이에 대한 이야기가 종종 화두로 떠오르곤 한다.

 

옛날에는 foreach가 가비지를 많이 생성해서 foreach 사용을 피하곤 했다고 하고, 예전 코드를 보다가 가비지 생성을 피하기 위해서인지 List에 Dictionary의 key를 저장하는 식으로 사용하는 것도 보았다.

 

그래서 이제는 foreach에서 가비지가 정말 안 나오는지와 for와 foreach의 성능 비교 확인해봤다.

 

기껏 테스트 환경을 만들어놨는데 로그를 무조건 띄우게 되어있어서 GC.Alloc()이 집계되는지라 Start()와 Update()에 따로 작성해주었다.

아무튼, int로 된 Dictionary를 만들어서 Update()에서 foreach로 순회했지만 GC Alloc은 하나도 발생하지 않았다. 이제는 가비지가 발생하지 않는 것이 맞다.

 

[Unity] Foreach의 GC의 원인 및 수정 (tistory.com)

 

[Unity] Foreach의 GC의 원인 및 수정

Unity 5.5 미만 버전에서 foreach를 사용할 경우 가비지가 발생하는 이슈가 있었다. 원인 - Mono C# Unity 버전에서 foreach loop 종료 시 값을 강제로 박싱 - 값 형식의 열거자를 생성하였는데, 해당 열거자

everyday-devup.tistory.com

+기존의 foreach에서 가비지가 발생했던 원인은 값 타입을 interface(참조 형식)로 박싱했기 때문이고, 현재는 포인터를 직접 넘겨주는 식으로 수정되어 가비지가 발생하지 않는다고 한다.

 

using System.Collections.Generic;
using System.Linq;
using UnityEngine;

public partial class Tester : MonoBehaviour
{
    List<int> globalIntList = new List<int>();
    List<string> globalStringList = new List<string>();

    private void ForAndForeachTest()
    {
        List<int> preparedIntList = new List<int>();

        int temp = 0;

        for (int i = 0; i < _repeatTime; ++i)
        {
            preparedIntList.Add(i);
        }

        DoTest("List Traversal using for", repeatTime => {
            for (int i = 0; i < repeatTime; ++i)
            {
                temp = preparedIntList[i];
            }
        });

        DoTest("List Traversal using foreach", repeatTime => {
            foreach (var number in preparedIntList)
            {
                temp = number;
            }
        });

        Dictionary<int, int> intDictionary = new Dictionary<int, int>();

        for (int i = 0; i < _repeatTime; ++i)
        {
            intDictionary.Add(i, i);
            globalIntList.Add(i);
        }

        DoTest("int Dictionary Traversal using foreach", repeatTime => {
            foreach (var s in intDictionary)
            {
                temp = s.Value;
            }
        });

        DoTest("int Dictionary Traversal using Global List + for", repeatTime => {
            for (int i = 0; i < repeatTime; ++i)
            {
                temp = intDictionary[globalIntList[i]];
            }
        });

        DoTest("int Dictionary Traversal using new Key List + for", repeatTime => {
            List<int> newKeyList = intDictionary.Keys.ToList();

            for (int i = 0; i < repeatTime; ++i)
            {
                temp = intDictionary[newKeyList[i]];
            }
        });

        DoTest("int Dictionary Traversal using prepared List + for", repeatTime => {
            for (int i = 0; i < repeatTime; ++i)
            {
                temp = intDictionary[preparedIntList[i]];
            }
        });

        Dictionary<string, string> stringDictionary = new Dictionary<string, string>();
        List<string> stringList = new List<string>();

        string stringTemp = "";

        for (int i = 0; i < _repeatTime; ++i)
        {
            globalStringList.Add(i.ToString());
            stringDictionary.Add(i.ToString(), i.ToString());
            stringList.Add(i.ToString());
        }

        DoTest("string Dictionary Traversal using Global List + for", repeatTime => {
            for (int i = 0; i < repeatTime; ++i)
            {
                stringTemp = stringDictionary[globalStringList[i]];
            }
        });

        DoTest("string Dictionary Traversal using new Key List + for", repeatTime => {
            List<string> newKeyList = stringDictionary.Keys.ToList();

            for (int i = 0; i < repeatTime; ++i)
            {
                stringTemp = stringDictionary[newKeyList[i]];
            }
        });

        DoTest("string Dictionary Traversal using prepared List + for", repeatTime => {
            for (int i = 0; i < repeatTime; ++i)
            {
                stringTemp = stringDictionary[stringList[i]];
            }
        });
    }
}

 

 

결과를 보면 List를 사용할 때는 for문이 foreach보다 대략 50~60% 정도 빠르다.

Dictionary를 사용할 때는 for문으로 돌려면 List를 따로 만들어야 하는데, 미리 key로 이루어진 List를 만들어서 순회해도 foreach로 도는 것보다 2배 가량 느리다.

 

나머지는 key가 포함된 List를 전역 변수로 하는가, 지역 변수로 하는가, 람다식 내부에 만들어주는가에 따라서 속도 차이가 있는 것 같아서 차이를 보려고 했는데, 실행할 때마다, 또 껐다 킬 때마다 속도가 들쭉날쭉하다. 처음에는 Keys.ToList()가 제일 빨라서 가까운 곳에 선언되서 접근 속도가 빠른가보다 생각했는데, 별 관계 없는 것 같다.

 

근데 어차피 foreach가 빨라서 굳이 List로 만들어서 사용할 필요가 없다.

 

결론적으로 List나 배열 등 index로 접근 가능한 경우에는 for문을 사용하는 게 속도도 빠르고, 데이터를 제어하기도 편리하니 for문을 쓰고, Dictionary를 사용할 때는 걱정 없이 foreach를 사용하면 될 것 같다.