برچسب: Tips

  • Top 6 Performance Tips when dealing with strings in C# 12 and .NET 8 | Code4IT

    Top 6 Performance Tips when dealing with strings in C# 12 and .NET 8 | Code4IT


    Small changes sometimes make a huge difference. Learn these 6 tips to improve the performance of your application just by handling strings correctly.

    Table of Contents

    Just a second! 🫷
    If you are here, it means that you are a software developer.
    So, you know that storage, networking, and domain management have a cost .

    If you want to support this blog, please ensure that you have disabled the adblocker for this site.
    I configured Google AdSense to show as few ADS as possible – I don’t want to bother you with lots of ads, but I still need to add some to pay for the resources for my site.

    Thank you for your understanding.
    Davide

    Sometimes, just a minor change makes a huge difference. Maybe you won’t notice it when performing the same operation a few times. Still, the improvement is significant when repeating the operation thousands of times.

    In this article, we will learn five simple tricks to improve the performance of your application when dealing with strings.

    Note: this article is part of C# Advent Calendar 2023, organized by Matthew D. Groves: it’s maybe the only Christmas tradition I like (yes, I’m kind of a Grinch 😂).

    Benchmark structure, with dependencies

    Before jumping to the benchmarks, I want to spend a few words on the tools I used for this article.

    The project is a .NET 8 class library running on a laptop with an i5 processor.

    Running benchmarks with BenchmarkDotNet

    I’m using BenchmarkDotNet to create benchmarks for my code. BenchmarkDotNet is a library that runs your methods several times, captures some metrics, and generates a report of the executions. If you follow my blog, you might know I’ve used it several times – for example, in my old article “Enum.HasFlag performance with BenchmarkDotNet”.

    All the benchmarks I created follow the same structure:

    [MemoryDiagnoser]
    public class BenchmarkName()
    {
        [Params(1, 5, 10)] // clearly, I won't use these values
        public int Size;
    
        public string[] AllStrings { get; set; }
    
        [IterationSetup]
        public void Setup()
        {
            AllStrings = StringArrayGenerator.Generate(Size, "hello!", "HELLO!");
        }
    
        [Benchmark(Baseline=true)]
        public void FirstMethod()
        {
            //omitted
        }
    
        [Benchmark]
        public void SecondMethod()
        {
            //omitted
        }
    }
    

    In short:

    • the class is marked with the [MemoryDiagnoser] attribute: the benchmark will retrieve info for both time and memory usage;
    • there is a property named Size with the attribute [Params]: this attribute lists the possible values for the Size property;
    • there is a method marked as [IterationSetup]: this method runs before every single execution, takes the value from the Size property, and initializes the AllStrings array;
    • the methods that are parts of the benchmark are marked with the [Benchmark] attribute.

    Generating strings with Bogus

    I relied on Bogus to create dummy values. This NuGet library allows you to generate realistic values for your objects with a great level of customization.

    The string array generation strategy is shared across all the benchmarks, so I moved it to a static method:

     public static class StringArrayGenerator
     {
         public static string[] Generate(int size, params string[] additionalStrings)
         {
             string[] array = new string[size];
             Faker faker = new Faker();
    
             List<string> fixedValues = [
                 string.Empty,
                 "   ",
                 "\n  \t",
                 null
             ];
    
             if (additionalStrings != null)
                 fixedValues.AddRange(additionalStrings);
    
             for (int i = 0; i < array.Length; i++)
             {
                 if (Random.Shared.Next() % 4 == 0)
                 {
                     array[i] = Random.Shared.GetItems<string>(fixedValues.ToArray(), 1).First();
                 }
                 else
                 {
                     array[i] = faker.Lorem.Word();
                 }
             }
    
             return array;
         }
     }
    

    Here I have a default set of predefined values ([string.Empty, " ", "\n \t", null]), which can be expanded with the values coming from the additionalStrings array. These values are then placed in random positions of the array.

    In most cases, though, the value of the string is defined by Bogus.

    Generating plots with chartbenchmark.net

    To generate the plots you will see in this article, I relied on chartbenchmark.net, a fantastic tool that transforms the output generated by BenchmarkDotNet on the console in a dynamic, customizable plot. This tool created by Carlos Villegas is available on GitHub, and it surely deserves a star!

    Please note that all the plots in this article have a Log10 scale: this scale allows me to show you the performance values of all the executions in the same plot. If I used the Linear scale, you would be able to see only the biggest values.

    We are ready. It’s time to run some benchmarks!

    Tip #1: StringBuilder is (almost always) better than String Concatenation

    Let’s start with a simple trick: if you need to concatenate strings, using a StringBuilder is generally more efficient than concatenating string.

    [MemoryDiagnoser]
    public class StringBuilderVsConcatenation()
    {
        [Params(4, 100, 10_000, 100_000)]
        public int Size;
    
        public string[] AllStrings { get; set; }
    
        [IterationSetup]
        public void Setup()
        {
            AllStrings = StringArrayGenerator.Generate(Size, "hello!", "HELLO!");
        }
    
        [Benchmark]
        public void WithStringBuilder()
        {
            StringBuilder sb = new StringBuilder();
    
            foreach (string s in AllStrings)
            {
                sb.Append(s);
            }
    
            var finalString = sb.ToString();
        }
    
        [Benchmark]
        public void WithConcatenation()
        {
            string finalString = "";
            foreach (string s in AllStrings)
            {
                finalString += s;
            }
        }
    }
    

    Whenever you concatenate strings with the + sign, you create a new instance of a string. This operation takes some time and allocates memory for every operation.

    On the contrary, using a StringBuilder object, you can add the strings in memory and generate the final string using a performance-wise method.

    Here’s the result table:

    Method Size Mean Error StdDev Median Ratio RatioSD Allocated Alloc Ratio
    WithStringBuilder 4 4.891 us 0.5568 us 1.607 us 4.750 us 1.00 0.00 1016 B 1.00
    WithConcatenation 4 3.130 us 0.4517 us 1.318 us 2.800 us 0.72 0.39 776 B 0.76
    WithStringBuilder 100 7.649 us 0.6596 us 1.924 us 7.650 us 1.00 0.00 4376 B 1.00
    WithConcatenation 100 13.804 us 1.1970 us 3.473 us 13.800 us 1.96 0.82 51192 B 11.70
    WithStringBuilder 10000 113.091 us 4.2106 us 12.081 us 111.000 us 1.00 0.00 217200 B 1.00
    WithConcatenation 10000 74,512.259 us 2,111.4213 us 6,058.064 us 72,593.050 us 666.43 91.44 466990336 B 2,150.05
    WithStringBuilder 100000 1,037.523 us 37.1009 us 108.225 us 1,012.350 us 1.00 0.00 2052376 B 1.00
    WithConcatenation 100000 7,469,344.914 us 69,720.9843 us 61,805.837 us 7,465,779.900 us 7,335.08 787.44 46925872520 B 22,864.17

    Let’s see it as a plot.

    Beware of the scale in the diagram!: it’s a Log10 scale, so you’d better have a look at the value displayed on the Y-axis.

    StringBuilder vs string concatenation in C#: performance benchmark

    As you can see, there is a considerable performance improvement.

    There are some remarkable points:

    1. When there are just a few strings to concatenate, the + operator is more performant, both on timing and allocated memory;
    2. When you need to concatenate 100000 strings, the concatenation is ~7000 times slower than the string builder.

    In conclusion, use the StringBuilder to concatenate more than 5 or 6 strings. Use the string concatenation for smaller operations.

    Edit 2024-01-08: turn out that string.Concat has an overload that accepts an array of strings. string.Concat(string[]) is actually faster than using the StringBuilder. Read more this article by Robin Choffardet.

    Tip #2: EndsWith(string) vs EndsWith(char): pick the right overload

    One simple improvement can be made if you use StartsWith or EndsWith, passing a single character.

    There are two similar overloads: one that accepts a string, and one that accepts a char.

    [MemoryDiagnoser]
    public class EndsWithStringVsChar()
    {
        [Params(100, 1000, 10_000, 100_000, 1_000_000)]
        public int Size;
    
        public string[] AllStrings { get; set; }
    
        [IterationSetup]
        public void Setup()
        {
            AllStrings = StringArrayGenerator.Generate(Size);
        }
    
        [Benchmark(Baseline = true)]
        public void EndsWithChar()
        {
        foreach (string s in AllStrings)
        {
            _ = s?.EndsWith('e');
        }
        }
    
        [Benchmark]
        public void EndsWithString()
        {
        foreach (string s in AllStrings)
        {
            _ = s?.EndsWith("e");
        }
        }
    }
    

    We have the following results:

    Method Size Mean Error StdDev Median Ratio
    EndsWithChar 100 2.189 us 0.2334 us 0.6771 us 2.150 us 1.00
    EndsWithString 100 5.228 us 0.4495 us 1.2970 us 5.050 us 2.56
    EndsWithChar 1000 12.796 us 1.2006 us 3.4831 us 12.200 us 1.00
    EndsWithString 1000 30.434 us 1.8783 us 5.4492 us 29.250 us 2.52
    EndsWithChar 10000 25.462 us 2.0451 us 5.9658 us 23.950 us 1.00
    EndsWithString 10000 251.483 us 18.8300 us 55.2252 us 262.300 us 10.48
    EndsWithChar 100000 209.776 us 18.7782 us 54.1793 us 199.900 us 1.00
    EndsWithString 100000 826.090 us 44.4127 us 118.5465 us 781.650 us 4.14
    EndsWithChar 1000000 2,199.463 us 74.4067 us 217.0480 us 2,190.600 us 1.00
    EndsWithString 1000000 7,506.450 us 190.7587 us 562.4562 us 7,356.250 us 3.45

    Again, let’s generate the plot using the Log10 scale:

    EndsWith(char) vs EndsWith(string) in C# performance benchmark

    They appear to be almost identical, but look closely: based on this benchmark, when we have 10000, using EndsWith(string) is 10x slower than EndsWith(char).

    Also, here, the duration ratio on the 1.000.000-items array is ~3.5. At first, I thought there was an error on the benchmark, but when rerunning it on the benchmark, the ratio did not change.

    It looks like you have the best improvement ratio when the array has ~10.000 items.

    Tip #3: IsNullOrEmpty vs IsNullOrWhitespace vs IsNullOrEmpty + Trim

    As you might know, string.IsNullOrWhiteSpace performs stricter checks than string.IsNullOrEmpty.

    (If you didn’t know, have a look at this quick explanation of the cases covered by these methods).

    Does it affect performance?

    To demonstrate it, I have created three benchmarks: one for string.IsNullOrEmpty, one for string.IsNullOrWhiteSpace, and another one that lays in between: it first calls Trim() on the string, and then calls string.IsNullOrEmpty.

    [MemoryDiagnoser]
    public class StringEmptyBenchmark
    {
        [Params(100, 1000, 10_000, 100_000, 1_000_000)]
        public int Size;
    
        public string[] AllStrings { get; set; }
    
        [IterationSetup]
        public void Setup()
        {
            AllStrings = StringArrayGenerator.Generate(Size);
        }
    
        [Benchmark(Baseline = true)]
        public void StringIsNullOrEmpty()
        {
            foreach (string s in AllStrings)
            {
                _ = string.IsNullOrEmpty(s);
            }
        }
    
        [Benchmark]
        public void StringIsNullOrEmptyWithTrim()
        {
            foreach (string s in AllStrings)
            {
                _ = string.IsNullOrEmpty(s?.Trim());
            }
        }
    
        [Benchmark]
        public void StringIsNullOrWhitespace()
        {
            foreach (string s in AllStrings)
            {
                _ = string.IsNullOrWhiteSpace(s);
            }
        }
    }
    

    We have the following values:

    Method Size Mean Error StdDev Ratio
    StringIsNullOrEmpty 100 1.723 us 0.2302 us 0.6715 us 1.00
    StringIsNullOrEmptyWithTrim 100 2.394 us 0.3525 us 1.0282 us 1.67
    StringIsNullOrWhitespace 100 2.017 us 0.2289 us 0.6604 us 1.45
    StringIsNullOrEmpty 1000 10.885 us 1.3980 us 4.0781 us 1.00
    StringIsNullOrEmptyWithTrim 1000 20.450 us 1.9966 us 5.8240 us 2.13
    StringIsNullOrWhitespace 1000 13.160 us 1.0851 us 3.1482 us 1.34
    StringIsNullOrEmpty 10000 18.717 us 1.1252 us 3.2464 us 1.00
    StringIsNullOrEmptyWithTrim 10000 52.786 us 1.2208 us 3.5222 us 2.90
    StringIsNullOrWhitespace 10000 46.602 us 1.2363 us 3.4668 us 2.54
    StringIsNullOrEmpty 100000 168.232 us 12.6948 us 36.0129 us 1.00
    StringIsNullOrEmptyWithTrim 100000 439.744 us 9.3648 us 25.3182 us 2.71
    StringIsNullOrWhitespace 100000 394.310 us 7.8976 us 20.5270 us 2.42
    StringIsNullOrEmpty 1000000 2,074.234 us 64.3964 us 186.8257 us 1.00
    StringIsNullOrEmptyWithTrim 1000000 4,691.103 us 112.2382 us 327.4040 us 2.28
    StringIsNullOrWhitespace 1000000 4,198.809 us 83.6526 us 161.1702 us 2.04

    As you can see from the Log10 table, the results are pretty similar:

    string.IsNullOrEmpty vs string.IsNullOrWhiteSpace vs Trim in C#: performance benchmark

    On average, StringIsNullOrWhitespace is ~2 times slower than StringIsNullOrEmpty.

    So, what should we do? Here’s my two cents:

    1. For all the data coming from the outside (passed as input to your system, received from an API call, read from the database), use string.IsNUllOrWhiteSpace: this way you can ensure that you are not receiving unexpected data;
    2. If you read data from an external API, customize your JSON deserializer to convert whitespace strings as empty values;
    3. Needless to say, choose the proper method depending on the use case. If a string like “\n \n \t” is a valid value for you, use string.IsNullOrEmpty.

    Tip #4: ToUpper vs ToUpperInvariant vs ToLower vs ToLowerInvariant: they look similar, but they are not

    Even though they look similar, there is a difference in terms of performance between these four methods.

    [MemoryDiagnoser]
    public class ToUpperVsToLower()
    {
        [Params(100, 1000, 10_000, 100_000, 1_000_000)]
        public int Size;
    
        public string[] AllStrings { get; set; }
    
        [IterationSetup]
        public void Setup()
        {
            AllStrings = StringArrayGenerator.Generate(Size);
        }
    
        [Benchmark]
        public void WithToUpper()
        {
            foreach (string s in AllStrings)
            {
                _ = s?.ToUpper();
            }
        }
    
        [Benchmark]
        public void WithToUpperInvariant()
        {
            foreach (string s in AllStrings)
            {
                _ = s?.ToUpperInvariant();
            }
        }
    
        [Benchmark]
        public void WithToLower()
        {
            foreach (string s in AllStrings)
            {
                _ = s?.ToLower();
            }
        }
    
        [Benchmark]
        public void WithToLowerInvariant()
        {
            foreach (string s in AllStrings)
            {
                _ = s?.ToLowerInvariant();
            }
        }
    }
    

    What will this benchmark generate?

    Method Size Mean Error StdDev Median P95 Ratio
    WithToUpper 100 9.153 us 0.9720 us 2.789 us 8.200 us 14.980 us 1.57
    WithToUpperInvariant 100 6.572 us 0.5650 us 1.639 us 6.200 us 9.400 us 1.14
    WithToLower 100 6.881 us 0.5076 us 1.489 us 7.100 us 9.220 us 1.19
    WithToLowerInvariant 100 6.143 us 0.5212 us 1.529 us 6.100 us 8.400 us 1.00
    WithToUpper 1000 69.776 us 9.5416 us 27.833 us 68.650 us 108.815 us 2.60
    WithToUpperInvariant 1000 51.284 us 7.7945 us 22.860 us 38.700 us 89.290 us 1.85
    WithToLower 1000 49.520 us 5.6085 us 16.449 us 48.100 us 79.110 us 1.85
    WithToLowerInvariant 1000 27.000 us 0.7370 us 2.103 us 26.850 us 30.375 us 1.00
    WithToUpper 10000 241.221 us 4.0480 us 3.588 us 240.900 us 246.560 us 1.68
    WithToUpperInvariant 10000 339.370 us 42.4036 us 125.028 us 381.950 us 594.760 us 1.48
    WithToLower 10000 246.861 us 15.7924 us 45.565 us 257.250 us 302.875 us 1.12
    WithToLowerInvariant 10000 143.529 us 2.1542 us 1.910 us 143.500 us 146.105 us 1.00
    WithToUpper 100000 2,165.838 us 84.7013 us 223.137 us 2,118.900 us 2,875.800 us 1.66
    WithToUpperInvariant 100000 1,885.329 us 36.8408 us 63.548 us 1,894.500 us 1,967.020 us 1.41
    WithToLower 100000 1,478.696 us 23.7192 us 50.547 us 1,472.100 us 1,571.330 us 1.10
    WithToLowerInvariant 100000 1,335.950 us 18.2716 us 35.203 us 1,330.100 us 1,404.175 us 1.00
    WithToUpper 1000000 20,936.247 us 414.7538 us 1,163.014 us 20,905.150 us 22,928.350 us 1.64
    WithToUpperInvariant 1000000 19,056.983 us 368.7473 us 287.894 us 19,085.400 us 19,422.880 us 1.41
    WithToLower 1000000 14,266.714 us 204.2906 us 181.098 us 14,236.500 us 14,593.035 us 1.06
    WithToLowerInvariant 1000000 13,464.127 us 266.7547 us 327.599 us 13,511.450 us 13,926.495 us 1.00

    Let’s see it as the usual Log10 plot:

    ToUpper vs ToLower comparison in C#: performance benchmark

    We can notice a few points:

    1. The ToUpper family is generally slower than the ToLower family;
    2. The Invariant family is faster than the non-Invariant one; we will see more below;

    So, if you have to normalize strings using the same casing, ToLowerInvariant is the best choice.

    Tip #5: OrdinalIgnoreCase vs InvariantCultureIgnoreCase: logically (almost) equivalent, but with different performance

    Comparing strings is trivial: the string.Compare method is all you need.

    There are several modes to compare strings: you can specify the comparison rules by setting the comparisonType parameter, which accepts a StringComparison value.

    [MemoryDiagnoser]
    public class StringCompareOrdinalVsInvariant()
    {
        [Params(100, 1000, 10_000, 100_000, 1_000_000)]
        public int Size;
    
        public string[] AllStrings { get; set; }
    
        [IterationSetup]
        public void Setup()
        {
            AllStrings = StringArrayGenerator.Generate(Size, "hello!", "HELLO!");
        }
    
        [Benchmark(Baseline = true)]
        public void WithOrdinalIgnoreCase()
        {
            foreach (string s in AllStrings)
            {
                _ = string.Equals(s, "hello!", StringComparison.OrdinalIgnoreCase);
            }
        }
    
        [Benchmark]
        public void WithInvariantCultureIgnoreCase()
        {
            foreach (string s in AllStrings)
            {
                _ = string.Equals(s, "hello!", StringComparison.InvariantCultureIgnoreCase);
            }
        }
    }
    

    Let’s see the results:

    Method Size Mean Error StdDev Ratio
    WithOrdinalIgnoreCase 100 2.380 us 0.2856 us 0.8420 us 1.00
    WithInvariantCultureIgnoreCase 100 7.974 us 0.7817 us 2.3049 us 3.68
    WithOrdinalIgnoreCase 1000 11.316 us 0.9170 us 2.6603 us 1.00
    WithInvariantCultureIgnoreCase 1000 35.265 us 1.5455 us 4.4591 us 3.26
    WithOrdinalIgnoreCase 10000 20.262 us 1.1801 us 3.3668 us 1.00
    WithInvariantCultureIgnoreCase 10000 225.892 us 4.4945 us 12.5289 us 11.41
    WithOrdinalIgnoreCase 100000 148.270 us 11.3234 us 32.8514 us 1.00
    WithInvariantCultureIgnoreCase 100000 1,811.144 us 35.9101 us 64.7533 us 12.62
    WithOrdinalIgnoreCase 1000000 2,050.894 us 59.5966 us 173.8460 us 1.00
    WithInvariantCultureIgnoreCase 1000000 18,138.063 us 360.1967 us 986.0327 us 8.87

    As you can see, there’s a HUGE difference between Ordinal and Invariant.

    When dealing with 100.000 items, StringComparison.InvariantCultureIgnoreCase is 12 times slower than StringComparison.OrdinalIgnoreCase!

    Ordinal vs InvariantCulture comparison in C#: performance benchmark

    Why? Also, why should we use one instead of the other?

    Have a look at this code snippet:

    var s1 = "Aa";
    var s2 = "A" + new string('\u0000', 3) + "a";
    
    string.Equals(s1, s2, StringComparison.InvariantCultureIgnoreCase); //True
    string.Equals(s1, s2, StringComparison.OrdinalIgnoreCase); //False
    

    As you can see, s1 and s2 represent equivalent, but not equal, strings. We can then deduce that OrdinalIgnoreCase checks for the exact values of the characters, while InvariantCultureIgnoreCase checks the string’s “meaning”.

    So, in most cases, you might want to use OrdinalIgnoreCase (as always, it depends on your use case!)

    Tip #6: Newtonsoft vs System.Text.Json: it’s a matter of memory allocation, not time

    For the last benchmark, I created the exact same model used as an example in the official documentation.

    This benchmark aims to see which JSON serialization library is faster: Newtonsoft or System.Text.Json?

    [MemoryDiagnoser]
    public class JsonSerializerComparison
    {
        [Params(100, 10_000, 1_000_000)]
        public int Size;
        List<User?> Users { get; set; }
    
        [IterationSetup]
        public void Setup()
        {
            Users = UsersCreator.GenerateUsers(Size);
        }
    
        [Benchmark(Baseline = true)]
        public void WithJson()
        {
            foreach (User? user in Users)
            {
                var asString = System.Text.Json.JsonSerializer.Serialize(user);
    
                _ = System.Text.Json.JsonSerializer.Deserialize<User?>(asString);
            }
        }
    
        [Benchmark]
        public void WithNewtonsoft()
        {
            foreach (User? user in Users)
            {
                string asString = Newtonsoft.Json.JsonConvert.SerializeObject(user);
                _ = Newtonsoft.Json.JsonConvert.DeserializeObject<User?>(asString);
            }
        }
    }
    

    As you might know, the .NET team has added lots of performance improvements to the JSON Serialization functionalities, and you can really see the difference!

    Method Size Mean Error StdDev Median Ratio RatioSD Gen0 Gen1 Allocated Alloc Ratio
    WithJson 100 2.063 ms 0.1409 ms 0.3927 ms 1.924 ms 1.00 0.00 292.87 KB 1.00
    WithNewtonsoft 100 4.452 ms 0.1185 ms 0.3243 ms 4.391 ms 2.21 0.39 882.71 KB 3.01
    WithJson 10000 44.237 ms 0.8787 ms 1.3936 ms 43.873 ms 1.00 0.00 4000.0000 1000.0000 29374.98 KB 1.00
    WithNewtonsoft 10000 78.661 ms 1.3542 ms 2.6090 ms 78.865 ms 1.77 0.08 14000.0000 1000.0000 88440.99 KB 3.01
    WithJson 1000000 4,233.583 ms 82.5804 ms 113.0369 ms 4,202.359 ms 1.00 0.00 484000.0000 1000.0000 2965741.56 KB 1.00
    WithNewtonsoft 1000000 5,260.680 ms 101.6941 ms 108.8116 ms 5,219.955 ms 1.24 0.04 1448000.0000 1000.0000 8872031.8 KB 2.99

    As you can see, Newtonsoft is 2x slower than System.Text.Json, and it allocates 3x the memory compared with the other library.

    So, well, if you don’t use library-specific functionalities, I suggest you replace Newtonsoft with System.Text.Json.

    Wrapping up

    In this article, we learned that even tiny changes can make a difference in the long run.

    Let’s recap some:

    1. Using StringBuilder is generally WAY faster than using string concatenation unless you need to concatenate 2 to 4 strings;
    2. Sometimes, the difference is not about execution time but memory usage;
    3. EndsWith and StartsWith perform better if you look for a char instead of a string. If you think of it, it totally makes sense!
    4. More often than not, string.IsNullOrWhiteSpace performs better checks than string.IsNullOrEmpty; however, there is a huge difference in terms of performance, so you should pick the correct method depending on the usage;
    5. ToUpper and ToLower look similar; however, ToLower is quite faster than ToUpper;
    6. Ordinal and Invariant comparison return the same value for almost every input; but Ordinal is faster than Invariant;
    7. Newtonsoft performs similarly to System.Text.Json, but it allocates way more memory.

    This article first appeared on Code4IT 🐧

    My suggestion is always the same: take your time to explore the possibilities! Toy with your code, try to break it, benchmark it. You’ll find interesting takes!

    I hope you enjoyed this article! Let’s keep in touch on Twitter or LinkedIn! 🤜🤛

    Happy coding!

    🐧





    Source link

  • 7 Useful Tips to Consider When Starting a Trucking Business


    There are many business lines in the world that are not easy to manage, and the trucking business is one of them. This industry is one of the booming industries in many countries.

    Nowadays, many business owners are trying to take part in this industry. Over the past years, this business has shown constant growth, which has made it a popular business line. If you are planning to start a trucking business, then you have to understand the complex jargon of this field. Along with that, you need to get a DOT authority for operating a business in your State.

    In this blog, you will find out how you can start and run your trucking business successfully.

    Do your research

    To hit the jackpot, the first thing you need to do is to crack the nuts. This means you will have to research the market and needs.

    By doing in-depth research, you will be able to identify your business niche in the trucking industry. Are you interested in transporting goods or using a truck for mobile billboards? These are only two examples, but when you research it, you will definitely find more possibilities in it.

    After that, it will be easy for you to develop a business plan.

    Find your target market

    Another one of the leading business strategies is finding and understanding the target audience. Once you understand for whom you will offer your services and what their needs are, it will become easy for you to offer the services and make more sales.

    It will be a wise decision if you develop your business strategy according to the niche market. By following this approach, you can ensure that your operations are cohesive and on track. When you tailor your trucking services according to the needs of your clients, in results your business will be able to earn a reputation and revenue.

    Finance your fleet

    Businesses are all about heavy investment, no matter the size or scale of your startup. When it comes to the trucking business, you will be surprised to know the buying cost of trucks. When planning the finances for buying trucks, you will also have to prepare for the maintenance costs. You can find many financing options to start your business.

    You can also start your own company with new vehicles or can consider investing in offers for used commercial vehicles and construction machinery.

    Make it legal

    It is crucial for business owners to meet all the legal requirements to operate their businesses in State. Without legal recognition or approval, the federal ministry can take charge of you, and you could end up losing your business.

    Many people enter the trucking business without knowing that it is highly regulated. You will need to get a permit or authority to operate your business activities interstate. You will also need to file for a DOT MC Number in your State.

    Ensure that your business complies with the applicable laws for maintaining legitimacy.

    Invest on technology

    Technology is the future, and especially for trucking business startups, you should realize its importance earlier. Technology is about to dominate services and different businesses. With technology, you will provide numerous benefits to your business.

    When it comes to transporting business, you will have to track and manage the orders. For this, it is crucial for you to use mobile applications or websites to promote your business and make it visible. If you cannot afford oversized technological items in your business, you can still add basics like GPS systems, smart cameras, and more.

    Learn your competition

    When you research your market, you should also study your competitors. It will help you to understand the threats and weaknesses that already existing businesses are facing. This way, you will come up with innovative business strategies and fill the needs of the clients.

    You can also offer the most competitive prices from other truckers and brokers with reasonable margins, so a good number of clients will attract your business.

    Pro tip:

    You should always connect directly with consigners so you will pass the benefits to your clients through a reduction in prices.

    Final note:

    There is no doubt in it that the trucking business has been booming over the years, and it has brought gold for owners. If you get the fundamentals right, being new in the market, you can also harvest the jackpot.



    Source link

  • Golden Tips To Improve Your Essay Writing Skills


    Writing an essay is one of the many tasks you’ll face as a student, so it’s essential to have good essay-writing skills. Strong writing skills can help you excel in your classes, standardized tests, and workplace. Fortunately, there are many ways you can improve your essay-writing skills. This article will provide golden tips to help you become a better essay writer.

    Seek Professional Writing Help

    Seeking professional writing help is one of the golden tips to improve your essay writing skills because it gives you access to experienced and knowledgeable writers who can help you craft a high-quality essay. Essay writing can be challenging and time-consuming for many students, particularly those needing strong writing skills or more confidence in writing a good essay.

    With professional writing help, you can get personalized feedback, guidance, and support to ensure your essay is of the highest quality. Professional writing help can also allow you to learn from the expertise of experienced writers, enabling you to improve your essay-writing skills. Students can look into platforms like www.vivaessays.com/essay-writer/ to get the needed assistance.

    Read Widely

    Another crucial tip for improving your essay writing skills is to read widely. Reading other people’s work can give you a better insight into what makes a good essay and can help you to develop your writing style. Reading other people’s work can also help gain new knowledge and ideas.

    Additionally, reading widely allows you to better understand grammar and sentence structure, which will help you construct your sentences. Finally, reading widely can help you develop your critical thinking skills and allow you to compare and contrast different ideas and viewpoints. All of these skills will be beneficial when writing your essays.

    Practice!

    They say that practice makes perfect, and this is certainly true when it comes to essay writing. You can improve your essays by consistently practicing and honing your writing skills. Practicing can help you become more comfortable with the structure of an essay and become familiar with the conventions of essay writing.

    Additionally, practicing can help you become more aware of which words and phrases work best in an essay, as well as help you become a more effective and clear communicator. Practicing can also help you become more confident in your writing and can help you identify any weak areas that need improvement. In short, practicing can help you hone your skills and make you a better essay writer.

    Have Someone Else Review Your Work

    Having a third eye review of your work can help you identify areas of improvement in your essay-writing skills. It can see you identify areas where you may be using too many words or where your writing may be confusing or unclear. It can also aid in identifying areas where you may be making the same mistakes or where you may be repeating yourself. Furthermore, it can help you identify weak points in your argument or areas where you may need to provide more evidence or detail.

    Finally, it can help you identify any grammar, spelling, or punctuation mistakes that you may have made. Ultimately, having someone review your work can help you become a better essay writer by highlighting areas you need to improve and providing constructive feedback.

    Have A Study Buddy

    Having a study buddy or group can help improve your essay-writing skills by providing a constructive environment for peer review. The group members can read each other’s work, offer feedback and criticism, and discuss ways to improve the essay. This can help identify common mistakes and improvement areas and provide insight on how to structure an essay for clarity and effectiveness. Additionally, studying with a group can keep you motivated and on task. It can give a sense of camaraderie and support when tackling a complex writing task.

    Work On Your Grammar, Spelling, And Punctuation Skills

    Lastly, improving your grammar, spelling, and punctuation skills is essential for improving your essay writing skills. Good grammar, spelling, and punctuation are the foundation of effective communication. If your writing is filled with errors, your message may be lost, and your essay will not make the grade.

    Furthermore, when you write an essay, it is essential to remember the conventions of grammar, spelling, and punctuation. This will help ensure that your essay is straightforward to read. Additionally, if you can use correct grammar, spelling, and punctuation correctly, it will make your essay appear more professional and polished. Therefore, improving your grammar, spelling, and punctuation skills is essential to improving your essay writing skills.

    Conclusion

    Essays are part of every student’s life, so it’s crucial to have good essay-writing skills. Fortunately, there are many tips and strategies to help you become a better essay writer. These include seeking professional writing help, reading widely, practicing, having someone else review your work, and having a study buddy or group. Following these golden tips can improve your essay-writing skills and become a better essay writer.



    Source link

  • like Mermaid, but better. Syntax, installation, and practical usage tips &vert; Code4IT

    like Mermaid, but better. Syntax, installation, and practical usage tips | Code4IT


    D2 is an open-source tool to design architectural layouts using a declarative syntax. It’s a textual format, which can also be stored under source control. Let’s see how it works, how you can install it, and some practical usage tips.

    Table of Contents

    Just a second! 🫷
    If you are here, it means that you are a software developer.
    So, you know that storage, networking, and domain management have a cost .

    If you want to support this blog, please ensure that you have disabled the adblocker for this site.
    I configured Google AdSense to show as few ADS as possible – I don’t want to bother you with lots of ads, but I still need to add some to pay for the resources for my site.

    Thank you for your understanding.
    Davide

    When defining the architecture of a system, I believe in the adage that says that «A picture is worth a thousand words».

    Proper diagramming helps in understanding how the architecture is structured, the dependencies between components, how the different components communicate, and their responsibilities.

    A clear architectural diagram can also be useful for planning. Once you have a general idea of the components, you can structure the planning according to the module dependencies and the priorities.

    A lack of diagramming leads to a “just words” definition: how many times have you heard people talk about modules that do not exist or do not work as they were imagining?

    The whole team can benefit from having a common language: a clear diagram brings clear thoughts, helping all the stakeholders (developers, architects, managers) understand the parts that compose a system.

    I tried several approaches: both online WYSIWYG tools like Draw.IO and DSL like Structurizr and Mermaid. For different reasons, I wasn’t happy with any of them.

    Then I stumbled upon D2: its rich set of elements makes it my new go-to tool for describing architectures. Let’s see how it works!

    A quick guide to D2 syntax

    Just like the more famous Mermaid, when using D2, you have to declare all the elements and connections as textual nodes.

    You can generate diagrams online by using the Playground section available on the official website, or you can install it locally (as you will see later).

    Elements: the basic components of every diagram

    Elements are defined as a set of names that can be enriched with a label and other metadata.

    Here’s an example of the most straightforward configurations for standalone elements.

    service
    
    user: Application User
    
    job: {
      shape: hexagon
    }
    

    For each element, you can define its internal name (service), a label (user: Application User) and a shape (shape: hexagon).

    A simple diagram with only two unrelated elements

    Other than that, I love the fact that you can define elements to be displayed as multiple instances: this can be useful when a service has multiple instances of the same type, and you want to express it clearly without the need to manually create multiple elements.

    You can do it by setting the multiple property to true.

    apiGtw: API Gateway {
      shape: cloud
    }
    be: BackEnd {
      style.multiple: true
    }
    
    apiGtw -> be
    

    Simple diagram with multiple backends

    Grouping: nesting elements hierarchically

    You may want to group elements. You can do that by using a hierarchical structure.

    In the following example, the main container represents my e-commerce application, composed of a website and a background job. The website is composed of a frontend, a backend, and a database.

    ecommerce: E-commerce {
      website: User Website {
        frontend
        backend
        database: DB {
          shape: cylinder
        }
      }
    
      job: {
        shape: hexagon
      }
    }
    

    As you can see from the diagram definition, elements can be nested in a hierarchical structure using the {} symbols. Of course, you can still define styles and labels to nested elements.

    Diagram with nested elements

    Connections: making elements communicate

    An architectural diagram is helpful only if it can express connections between elements.

    To connect two elements, you must use the --, the -> or the <- connector. You have to link their IDs, not their labels.

    ecommerce: E-commerce {
        website: User Website {
            frontend
        backend
        database: DB {
            shape: cylinder
        }
        frontend -> backend
        backend -> database: retrieve records {
            style.stroke: red
        }
      }
    
      job: {
          shape: hexagon
      }
      job -> website.database: update records
    }
    

    The previous example contains some interesting points.

    • Elements within the same container can be referenced directly using their ID: frontend -> backend.
    • You can add labels to a connection: backend -> database: retrieve records.
    • You can apply styles to a connection, like choosing the arrow colour with style.stroke: red.
    • You can create connections between elements from different containers: job -> website.database.

    Connections between elements from different containers

    When referencing items from different containers, you must always include the container ID: job -> website.database works, but job -> database doesn’t because database is not defined (so it gets created from scratch).

    SQL Tables: represent the table schema

    An interesting part of D2 diagrams is the possibility of adding the description of SQL tables.

    Obviously, the structure cannot be validated: the actual syntax depends on the database vendor.

    However, having the table schema defined in the diagram can be helpful in reasoning around the dependencies needed to complete a development.

    serv: Products Service
    
    db: Database Schema {
      direction: right
      shape: cylinder
      userTable: dbo.user {
        shape: sql_table
        Id: int {constraint: primary_key}
        FirstName: text
        LastName: text
        Birthday: datetime2
      }
    
      productsTable: dbo.products {
        shape: sql_table
        Id: int {constraint: primary_key}
        Owner: int {constraint: foreign_key}
        Description: text
      }
    
      productsTable.Owner -> userTable.Id
    }
    
    serv -> db.productsTable: Retrieve products by user id
    

    Diagram with database tables

    Notice how you can also define constraints to an element, like {constraint: foreign_key}, and specify the references from one table to another.

    How to install and run D2 locally

    D2 is a tool written in Go.

    Go is not natively present in every computer, so you have to install it. You can learn how to install it from the official page.

    Once Go is ready, you can install D2 in several ways. I use Windows 11, so my preferred installation approach is to use a .msi installer, as described here.

    If you are on macOS, you can use Homebrew to install it by running:

    Regardless of the Operating System, you can have Go directly install D2 by running the following command:

    go install oss.terrastruct.com/d2@latest
    

    It’s even possible to install it via Docker. However, this approach is quite complex, so I prefer installing D2 directly with the other methods I explained before.

    You can find more information about the several installation approaches on the GitHub page of the project.

    Use D2 via command line

    To work with D2 diagrams, you need to create a file with the .d2 extension. That file will contain the textual representation of the diagrams, following the syntax we saw before.

    Once D2 is installed and the file is present in the file system (in my case, I named the file my-diagram.d2), you can use the console to generate the diagram locally – remember, I’m using Windows11, so I need to run the exe file:

    d2.exe --watch .\my-diagram.d2
    

    Now you can open your browser, head to the localhost page displayed on the shell, and see how D2 renders the local file. Thanks to the --watch flag, you can update the file locally and see the result appear on the browser without the need to restart the application.

    When the diagram is ready, you can export it as a PNG or SVG by running

    d2.exe .\my-diagram.d2 my-wonderful-design.png
    

    Create D2 Diagrams on Visual Studio Code

    Another approach is to install the D2 extension on VS Code.

    D2 extension on Visual Studio Code

    Thanks to this extension, you can open any D2 file and, by using the command palette, see a preview of the final result. You can also format the document to have the diagram definition tidy and well-structured.

    D2 extension command palette

    How to install and use D2 Diagrams on Obsidian

    Lastly, D2 can be easily integrated with tools like Obsidian. Among the community plugins, you can find the official D2 plugin.

    D2 plugin for Obsidian

    As you can imagine, Go is required on your machine.
    And, if necessary, you are required to explicitly set the path to the bin folder of Go. In my case, I had to set it to C:\Users\BelloneDavide\go\bin\.

    D2 plugin settings for Obsidian

    To insert a D2 diagram in a note generated with Obsidian, you have to use d2 as a code fence language.

    Practical tips for using D2

    D2 is easy to use once you have a basic understanding of how to create elements and connections.

    However, some tips may be useful to ease the process of creating the diagrams. Or, at least, these tips helped me write and maintain my diagrams.

    Separate elements and connections definition

    A good approach is to declare the application’s structure first, and then list all the connections between elements unless the elements are within the same components and are not expected to change.

    ecommerce: E-commerce {
      website: User Website {
        backend
        database: DB {
          shape: cylinder
        }
    
        backend -> database: retrieve records {
          style.stroke: red
        }
      }
    
      job -> website.database: update records
    }
    

    Here, the connection between backend and database is internal to the website element, so it makes sense to declare it directly within the website element.

    However, the other connection between the job and the database is cross-element. In the long run, it may bring readability problems.

    So, you could update it like this:

    ecommerce: E-commerce {
     website: User Website {
     backend
     database: DB {
     shape: cylinder
     }
    
     backend -> database: retrieve records {
     style.stroke: red
     }
     }
    
    - job -> website.database: update records
    }
    
    + ecommerce.job -> ecommerce.website.database: update records
    

    This tip can be extremely useful when you have more than one element with the same name belonging to different parents.

    Needless to say, since the order of the connection declarations does not affect the final rendering, write them in an organized way that best fits your needs. In general, I prefer creating sections (using comments to declare the area), and grouping connections by the outbound module.

    Pick a colour theme (and customize it, if you want!)

    D2 allows you to specify a theme for the diagram. There are some predefined themes (which are a set of colour palettes), each with a name and an ID.

    To use a theme, you have to specify it in the vars element on top of the diagram:

    vars: {
      d2-config: {
        theme-id: 103
      }
    }
    

    103 is the theme named “Earth tones”, using a brown-based palette that, when applied to the diagram, renders it like this.

    Diagram using the 103 colour palette

    However, if you have a preferred colour palette, you can use your own colours by overriding the default values:

    vars: {
      d2-config: {
        # Terminal theme code
        theme-id: 103
        theme-overrides: {
          B4: "#C5E1A5"
        }
      }
    }
    

    Diagram with a colour overridden

    You can read more about themes and customizations here.

    What is that B4 key overridden in the previous example? Unfortunately, I don’t know: you must try all the variables to understand how the diagram is rendered.

    Choose the right layout engine

    You can choose one of the three supported layout engines to render the elements in a different way (more info here).

    DAGRE and ELK are open source, but quite basic. TALA is more sophisticated, but it requires a paid licence.

    Here’s an example of how the same diagram is rendered using the three different engines.

    A comparison betweel DAGRE, ELK and TALA layout engines

    You can decide which engine to use by declaring it in the layout-engine element:

    vars: {
      d2-config: {
        layout-engine: tala
      }
    }
    

    Choosing the right layout engine can be beneficial because sometimes some elements are not rendered correctly: here’s a weird rendering with the DAGRE engine.

    DAGRE engine with a weird rendering

    Use variables to simplify future changes

    D2 allows you to define variables in a single place and have the same value repeated everywhere it’s needed.

    So, for example, instead of having

    mySystem: {
      reader: Magazine Reader
      writer: Magazine Writer
    }
    

    With the word “Magazine” repeated, you can move it to a variable, so that it can change in the future:

    vars: {
      entityName: Magazine
    }
    
    mySystem: {
      reader: ${entityName} Reader
      writer: ${entityName} Writer
    }
    

    If in the future you’ll have to handle not only Magazines but also other media types, you can simply replace the value of entityName in one place and have it updated all over the diagram.

    D2 vs Mermaid: a comparison

    D2 and Mermaid are similar but have some key differences.

    They both are diagram-as-a-code tools, meaning that the definition of a diagram is expressed as a text file, thus making it available under source control.

    Mermaid is already supported by many tools, like Azure DevOps wikis, GitHub pages, and so on.
    On the contrary, D2 must be installed (along with the Go language).

    Mermaid is quite a “close” system: even if it allows you to define some basic styles, it’s not that flexible.

    On the contrary, D2 allows you to choose a theme for the whole diagram, as well as choosing different layout engines.
    Also, D2 has some functionalities that are (currently) missing on Mermaid:

    Mermaid, on the contrary, allows us to define more types of diagrams: State Diagrams, Gantt, Mindmaps, and so on. Also, as we saw, it’s already supported on many platforms.

    So, my (current) choice is: use D2 for architectural diagrams, and use Mermaid for everything else.

    I haven’t tried D2 for Sequence Diagrams yet, so I won’t express an opinion on that.

    Further readings

    D2 is available online with a playground you can use to try things out in a sandboxed environment.

    🔗 D2 Playground

    All the documentation can be found on GitHub or on the official website:

    🔗 D2 documentation

    And, if you want, you can use icons to create better diagrams: D2 exposes a set of SVG icons that can be easily integrated into your diagrams. You can find them here:

    🔗 D2 predefined icons

    This article first appeared on Code4IT 🐧

    Ok, but diagrams have to live in a context. How can you create useful and maintainable documentation for your future self?

    A good way to document your architectural choices is to define ADRs (Architecture Decision Records), as explained here:

    🔗 Tracking decision with Architecture Decision Records (ADRs) | Code4IT

    And, of course, just the architectural diagram is not enough: you should also describe the dependencies, the constraints, the deployment strategies, and so on. Arc42 is a template that can guide you to proper system documentation:

    🔗 Arc42 Documentation, for a comprehensive description of your project | Code4IT

    Wrapping up

    I hope you enjoyed this article! Let’s keep in touch on LinkedIn, Twitter or BlueSky! 🤜🤛

    Happy coding!

    🐧





    Source link

  • 3 Simple Tips to Help Your Roofing Business Take Off


    In the competitive world of roofing, establishing a successful business can be challenging. As a roofer, you must differentiate yourself from the competition while ensuring customer satisfaction. In this article, we’ll explore three simple yet effective tips that can help take your roofing business to heights you never imagined. These strategies can pave the way for growth, increased visibility, and customer loyalty, ultimately boosting your bottom line. Read on to discover how investing in marketing, building business connections, and offering unique services can make all the difference in propelling your roofing enterprise forward.

    Invest In Marketing

    Investing in marketing is a crucial step in expanding the reach of your roofing business. By utilizing digital marketing strategies such as social media advertising and search engine optimization, you can effectively target potential customers searching for roofing services. This investment will not only increase your visibility but also establish your brand as a trusted name within the roofing industry.

    Traditional marketing methods, including direct mail campaigns and local community sponsorships, can also play a significant role in attracting new clients. As asphalt shingle roofs typically last between 15 to 30 years, homeowners will frequently require roof replacement or repairs during this timeframe. By maintaining a consistent presence in your community, you’ll ensure that your business is top of mind when these needs arise.

    Another key aspect of marketing is leveraging customer testimonials and reviews, which can build trust and credibility. Showcasing positive feedback from satisfied clients on your website and social media channels can greatly influence potential customers’ decisions. This trustworthiness will set your business apart from competitors who fail to prioritize customer satisfaction and engagement, making your marketing efforts even more rewarding.

    Make Business Connections

    Building strong business connections is essential for the growth and success of your roofing enterprise. Networking with other local businesses and joining industry associations can provide valuable opportunities for collaborations and referrals. By creating partnerships with businesses such as real estate agencies and home improvement stores, you can expand your customer base and increase your workload.

    Attending industry events and trade shows is another effective method for networking and making connections. It also allows you to stay up to date with industry trends and innovations, keeping your services competitive. Given that 33% of homeowners replace their roofs due to storm damage, collaborating with insurance companies can provide a steady stream of business during inclement weather seasons.

    Moreover, fostering relationships with suppliers can yield significant benefits for your roofing business. By negotiating bulk purchasing and discounts, you can reduce costs and improve your profit margins. Strong supplier relationships also ensure you receive quality materials promptly, allowing you to maintain the high standards that will earn your customers’ trust and repeat business.

    Offer Unique Services

    To stand out in the crowded roofing market, offering unique, innovative services can differentiate your business from competitors. Specializing in green roofing solutions or energy-efficient installations such as thermal insulation can attract environmentally conscious clients. Offering consultations and personalized solutions demonstrates a commitment to customer needs and expectations.

    Incorporating solar panel installation into your services can significantly boost your business, given the growing interest in renewable energy. According to the Solar Energy Industries Association, 97% of all solar installations in the United States are on residential rooftops, presenting a lucrative opportunity for roofing businesses. By training your team in solar technology, you can offer an additional service that aligns with current sustainability trends.

    Providing comprehensive service packages, including regular maintenance and emergency repairs, ensures you meet a wide range of customer needs. These all-inclusive offerings create a seamless experience for homeowners and foster long-term relationships. A robust service menu demonstrates your roofing business’s versatility and commitment to excellence, securing a loyal client base.

    Adopting effective marketing strategies, forging valuable business connections, and offering unique services can significantly boost your roofing business. These measures will not only increase your visibility and attract new clients but also create a strong foundation for long-term success. By embracing innovation and prioritizing customer satisfaction, you can ensure your business thrives in a competitive market. Start implementing these simple yet powerful strategies today and watch your roofing business reach new heights. Remember, the key to success lies in your commitment to growth, quality, and exceptional service.



    Source link