tonybaloney / CSnakes

https://tonybaloney.github.io/CSnakes/
MIT License
291 stars 21 forks source link

We make 3 heap allocations per item here. #172

Closed github-actions[bot] closed 2 months ago

github-actions[bot] commented 2 months ago

1. The item, which could be inlined as it's only used to call PyTuple_GetItem.

2. The key, which we need to recursively convert -- although if this is a string, which it mostly is, then we _could_ avoid this.

3. The value, which we need to recursively convert.

https://github.com/tonybaloney/CSnakes/blob/4090d5b794d8cabec10d75f723673a026167d65f/src/CSnakes.Runtime/PyObjectTypeConverter.Dictionary.cs#L32


using CSnakes.Runtime.CPython;
using CSnakes.Runtime.Python;
using System.Collections;
using System.Collections.ObjectModel;

namespace CSnakes.Runtime;
internal partial class PyObjectTypeConverter
{
    private object ConvertToDictionary(PyObject pyObject, Type destinationType, bool useMappingProtocol = false)
    {
        using PyObject items = useMappingProtocol ? 
            PyObject.Create(CPythonAPI.PyMapping_Items(pyObject)) : 
            PyObject.Create(CPythonAPI.PyDict_Items(pyObject));

        Type keyType = destinationType.GetGenericArguments()[0];
        Type valueType = destinationType.GetGenericArguments()[1];

        if (!knownDynamicTypes.TryGetValue(destinationType, out DynamicTypeInfo? typeInfo))
        {
            Type dictType = typeof(Dictionary<,>).MakeGenericType(keyType, valueType);
            Type returnType = typeof(ReadOnlyDictionary<,>).MakeGenericType(keyType, valueType);

            typeInfo = new(returnType.GetConstructor([dictType])!, dictType.GetConstructor([typeof(int)])!);
            knownDynamicTypes[destinationType] = typeInfo;
        }

        nint itemsLength = CPythonAPI.PyList_Size(items);
        IDictionary dict = (IDictionary)typeInfo.TransientTypeConstructor!.Invoke([(int)itemsLength]);

        for (nint i = 0; i < itemsLength; i++)
        {
            // TODO: We make 3 heap allocations per item here.
            // 1. The item, which could be inlined as it's only used to call PyTuple_GetItem.
            // 2. The key, which we need to recursively convert -- although if this is a string, which it mostly is, then we _could_ avoid this.
            // 3. The value, which we need to recursively convert.
            nint kvpTuple = CPythonAPI.PyList_GetItem(items, i);

            // Optimize keys as string because this is the most common case.
            if (keyType == typeof(string))
            {
                nint itemKey = CPythonAPI.PyTuple_GetItemWithNewRefRaw(kvpTuple, 0);
                using PyObject value = PyObject.Create(CPythonAPI.PyTuple_GetItemWithNewRefRaw(kvpTuple, 1));

                string? keyAsString = CPythonAPI.PyUnicode_AsUTF8Raw(itemKey);
                if (keyAsString is null)
                {
                    CPythonAPI.Py_DecRefRaw(itemKey);
                    throw PyObject.ThrowPythonExceptionAsClrException();
                }
                object? convertedValue = value.As(valueType);

                dict.Add(keyAsString!, convertedValue);
                CPythonAPI.Py_DecRefRaw(itemKey);
            } else
            {
                using PyObject key = PyObject.Create(CPythonAPI.PyTuple_GetItemWithNewRefRaw(kvpTuple, 0));
                using PyObject value = PyObject.Create(CPythonAPI.PyTuple_GetItemWithNewRefRaw(kvpTuple, 1));

                object? convertedKey = key.As(keyType);
                object? convertedValue = value.As(valueType);

                dict.Add(convertedKey!, convertedValue);
            }
            CPythonAPI.Py_DecRefRaw(kvpTuple);
        }

        return typeInfo.ReturnTypeConstructor.Invoke([dict]);
    }

    internal IReadOnlyDictionary<TKey, TValue> ConvertToDictionary<TKey, TValue>(PyObject pyObject) where TKey : notnull
    {
        using PyObject items = PyObject.Create(CPythonAPI.PyMapping_Items(pyObject));

        var dict = new Dictionary<TKey, TValue>();
        nint itemsLength = CPythonAPI.PyList_Size(items);
        for (nint i = 0; i < itemsLength; i++)
        {
            nint kvpTuple = CPythonAPI.PyList_GetItem(items, i);
            using PyObject key = PyObject.Create(CPythonAPI.PyTuple_GetItemWithNewRefRaw(kvpTuple, 0));
            using PyObject value = PyObject.Create(CPythonAPI.PyTuple_GetItemWithNewRefRaw(kvpTuple, 1));

            TKey convertedKey = key.As<TKey>();
            TValue convertedValue = value.As<TValue>();

            dict.Add(convertedKey, convertedValue);
            CPythonAPI.Py_DecRefRaw(kvpTuple);
        }

        return dict;
    }

    private PyObject ConvertFromDictionary(IDictionary dictionary)
    {
        int len = dictionary.Keys.Count;
        PyObject[] keys = new PyObject[len];
        PyObject[] values = new PyObject[len];

        int i = 0;
        foreach (DictionaryEntry kvp in dictionary)
        {
            keys[i] = ToPython(kvp.Key);
            values[i] = ToPython(kvp.Value);
            i++;
        }

        return Pack.CreateDictionary(keys, values);
    }
}