11
\$\begingroup\$

I wrote the following program to calculate n digits of Pi (where n could be anything, like 10M) in order to benchmark the CPU and it works perfectly (without OpenMP):

/*
*
* Simple PI Benchmarking tool
* Author: Suyash Srijan
* Email: suyashsrijan@outlook.com
*
* This program calculates how much time your CPU takes to compute n digits of PI using Chudnovsky Algorithm
* (http://en.wikipedia.org/wiki/Chudnovsky_algorithm) and uses the GNU Multiple Precision Arithmetic Library
* for computation.
*
* For verification of digits, you can download the digits from here: http://piworld.calico.jp/estart.html
*
* It's a single threaded program but you can compile it with OpenMP support to enable parallelization.
* WARN: OpenMP support is experimental
*
* Compile using gcc : gcc -O2 -Wall -o pibench pibench.c -lgmp -lssl -lcrypto
* Compile using gcc (with OpenMP): gcc -O2 -Wall -o pibench pibench.c -lgmp -lssl -lcrypto -fopenmp
*
*/

#include <gmp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sys/resource.h>
#include <sys/utsname.h>
#include <openssl/md5.h>

/* Import OpenMP header if compiling with -fopenmp */
#if defined(_OPENMP)
#include <omp.h>
#endif

/* You can't compile this on Windows */
#ifdef _WIN32
#error >>> Fatal: It is not possible to compile this program on Windows <<<
#endif

/* Build timestamp */
#define build_time __TIME__
#define build_date __DATE__

/* Calculate log to the base 2 using GCC's bit scan reverse intrinsic */
__inline__ unsigned int clc_log2(const unsigned int num) {
    return ((num <= 1) ? 0 : 32 - (__builtin_clz(num - 1)));
}

/* Calculate MD5 checksum for verification */
__inline__ char *clc_md5(const char *string) {
    MD5_CTX context;
    unsigned char digest[16];
    char *checksum = (char*)malloc(33);
    int i;

    MD5_Init(&context);
    MD5_Update(&context, string, strlen(string));
    MD5_Final(digest, &context);

    for (i = 0; i < 16; ++i) {
        snprintf(&(checksum[i*2]), 3, "%02x", (unsigned int)digest[i]);
    }
    return checksum;
}

/* Calculate pi digits main function */
__inline__ char *clc_pi(unsigned long dgts)
{
    /* Variable declaration */
    struct timespec start, end;
    unsigned long int i, ti, constant1, constant2, constant3;
    unsigned long iters = (dgts / 15) + 1;
    unsigned long precision;
    double bits;
    char *oput;
    mpz_t v1, v2, v3, v4, v5;
    mpf_t V1, V2, V3, total, tmp, res;
    mp_exp_t exponent;

    /* Initialize */
    constant1 = 545140134;
    constant2 = 13591409;
    constant3 = 640320;
    bits = clc_log2(10);
    precision = (dgts * bits) + 1;
    mpf_set_default_prec(precision);
    mpz_inits(v1, v2, v3, v4, v5, NULL);
    mpf_inits(res, tmp, V1, V2, V3, total, NULL);
    mpf_set_ui(total, 0);
    mpf_sqrt_ui(tmp, 10005);
    mpf_mul_ui(tmp, tmp, 426880);

    /* Get high-res time */
    clock_gettime(CLOCK_REALTIME, &start);

    /* Print total iterations and start computation of digits */
    printf("Total iterations: %lu\n\n", iters - 1);

#if defined(_OPENMP)
#pragma omp parallel for private(v1, v2, v3, v4, v5, V1, V2, V3, ti) reduction(+:total)
#endif

    /* Iterate and compute value using Chudnovsky Algorithm */
    for (i = 0x0; i < iters; i++) {
        ti = i * 3;
        mpz_fac_ui(v1, 6 * i);
        mpz_set_ui(v2, constant1);
        mpz_mul_ui(v2, v2, i);
        mpz_add_ui(v2, v2, constant2);
        mpz_fac_ui(v3, ti);
        mpz_fac_ui(v4, i);
        mpz_pow_ui(v4, v4, 3);
        mpz_ui_pow_ui(v5, constant3, ti);
        if ((1 & ti) == 1) { mpz_neg(v5, v5); }
        mpz_mul(v1, v1, v2);
        mpf_set_z(V1, v1);
        mpz_mul(v3, v3, v4);
        mpz_mul(v3, v3, v5);
        mpf_set_z(V2, v3);
        mpf_div(V3, V1, V2);
        mpf_add(total, total, V3);

        /* Print interations executed if debugging (I don't like spamming stdout unnecesarily) */
        #ifdef DEBUG
        printf("Iteration %lu of %lu successfully executed\n", i, iters - 1);
        #endif
    }

    /* Some final computations */
    mpf_ui_div(total, 1, total);
    mpf_mul(total, total, tmp);

    /* Get high-res time */
    clock_gettime(CLOCK_REALTIME, &end);

    /* Calculate and print time taken */
    double time_taken = (double)(end.tv_sec - start.tv_sec) + (double)(end.tv_nsec - start.tv_nsec) / 1E9;
    printf("Done!\n\nTime taken (seconds): %lf\n", time_taken);

    /* Store output */
    oput = mpf_get_str(NULL, &exponent, 10, dgts, total);

    /* Free up space consumed by variables */
    mpz_clears(v1, v2, v3, v4, v5, NULL);
    mpf_clears(res, tmp, V1, V2, V3, total, NULL);

    /* Return value */
    return oput;
}

/* Entry point of program */
int main(int argc, char *argv[]) {

    /* Set number of threads if compiling with -fopenmp */
#if defined(_OPENMP)
    omp_set_num_threads(8);
#endif

    /* Variable declaration and initialization */
    unsigned long how_many_digits = 10000;
    unsigned int base = 10;
    char *tmp_ptr;
    int pd = 0;
    int dd = 0;

    /* Try setting process priority to highest */
    int returnvalue = setpriority(PRIO_PROCESS, (id_t)0, -20);
    if (returnvalue == -1) { printf("WARN: Unable to max out priority. Did you not run this app as root?\n"); }

    /* Parse command line */
    if (argc == 3 && ((strcmp(argv[2], "--printdigits") == 0) || (strcmp(argv[2], "--nodigits") == 0) || (strcmp(argv[2], "--dumpdigits") == 0))) {
        how_many_digits = strtol(argv[1], &tmp_ptr, base);
        pd = (strcmp(argv[2], "--printdigits") == 0) ? 1 : 0;
        dd = (strcmp(argv[2], "--dumpdigits") == 0) ? 1 : 0; }

    /* Invalid command line parameters */
    else { fprintf(stderr, "Error: Invalid command-line arguments!\nUsage: pibench [digits] [parameter]\nParameter:\n--printdigits : Prints all digits on console\n--nodigits : Suppresses printing of digits on console\n--dumpdigits : Saves all the digits to a text file\n\nUsage example: pibench 50000 --printdigits\n"); exit(1); }

    /* Print introductory text */
    struct utsname uname_ptr;
    uname(&uname_ptr);
    printf("\n---------------------------------------------------------------");
    printf("\nPi Bench v1.0 beta (%s)\nBuild date: %s %s\n", uname_ptr.machine, build_date, build_time);
    printf("---------------------------------------------------------------\n\n");

    /* Check if digits isnt zero or below */
    if (how_many_digits < 1) { fprintf(stderr, "Error: Digit cannot be lower than 1\n"); exit(1); }

    /* Calculate digits of pi */
    printf("Computing %lu digits of PI...\n", how_many_digits);
    char *digits_of_pi = clc_pi(how_many_digits);

    /* Print the digits if user specified the --printdigits flag */
    if (pd == 1) {
        printf("Here are the digits:\n\n%.1s.%s\n", digits_of_pi, digits_of_pi + 1); }

    /* Save digits to text file if user specified the --dumpdigits flag */
    if (dd == 1) {
        FILE *file;
        if ((file = fopen("pidigits.txt", "w")) == NULL) {
            fprintf(stderr, "Error while opening file\n"); exit(-1); } else {
            fprintf(file, "%.1s.%s\n", digits_of_pi, digits_of_pi + 1);
            fclose(file); }
    }


    /* Print MD5 checksum */
    char *md5 = clc_md5(digits_of_pi);
    printf("MD5 checksum (for verification): %s\n", md5);

    /* Free the memory */
    free(digits_of_pi);

    /* Time to go! */
    printf("Goodbye!\n");
    return 0;
}

The source code is available here.

Any suggestions or tips will be greatly appreciated!

\$\endgroup\$
0
6
\$\begingroup\$

Just a few notes on some things I didn't see mentioned.

Compilation:

  • I originally couldn't compile the program with the command in the comments.

    /tmp/cc2H2h0a.o: In function 'clc_pi':
    test.c:(.text+0x148): undefined reference to 'clock_gettime'
    test.c:(.text+0x2f0): undefined referenceto 'clock_gettime'
    collect2: ld returned 1 exit status

    Add -lrt to the list of libraries you link to.

    // Compile using gcc : gcc -O2 -Wall -o pibench pibench.c -lgmp -lssl -lcrypto -lrt
    

Syntax:

  • The DEBUG stuff is distracting. Maybe it is temporary, but if you wanted to leave it in, I suggest extracting it:

    #include <stdarg.h>
    
    static inline void debug(const char *format, ...)
    {
    #ifdef DEBUG
        va_list ap;
        va_start(ap, format);
        vfprintf(stdout, format, ap);
        va_end(ap);
    #endif
    }
    

    and calling it:

    debug("Iteration %lu of %lu successfully executed\n", i, iters - 1);
    

    If DEBUG is undefined, the inline debug function will be empty and will be excluded during compilation - it disappears.

  • Put the else on its own line.

    if (dd == 1) {
        FILE *file;
        if ((file = fopen("pidigits.txt", "w")) == NULL) {
            fprintf(stderr, "Error while opening file\n"); exit(-1); } else {
            fprintf(file, "%.1s.%s\n", digits_of_pi, digits_of_pi + 1);
            fclose(file); }
    }
    

    When you use it this way, it is very easy to overlook it. I almost glanced over it when examining your code. There isn't really a reason to put it on it's own line, except to save LOC, which you could do better in other places.

    if (dd == 1) 
    {
        FILE *file;
        if ((file = fopen("pidigits.txt", "w")) == NULL) 
        {
            fprintf(stderr, "Error while opening file\n"); exit(-1); 
        } else {
            fprintf(file, "%.1s.%s\n", digits_of_pi, digits_of_pi + 1);
            fclose(file); 
        }
    }
    
  • Put all statements on separate lines. From Code Complete, 2nd Edition, pg. 759:

    With statements on their own lines, the code reads from top to bottom, instead of top to bottom and left to right. When you’re looking for a specific line of code, your eye should be able to follow the left margin of the code. It shouldn’t have to dip into each and every line just because a single line might contain two statements.

  • I would use more comments, especially around your OpenMP #pragmas and function calls.

  • Define i in your for loops.(C99)

    for (int i = 0x0; i < iters; i++)
    

Miscellaneous:

  • fopen(), a widely-used file I/O functions that you are using, got a facelift in C11. It now supports a new exclusive create-and-open mode (“...x“). The new mode behaves like O_CREAT|O_EXCL in POSIX and is commonly used for lock files. The “...x” family of modes includes the following options:

    • wx create text file for writing with exclusive access.

    • wbx create binary file for writing with exclusive access.

    • w+x create text file for update with exclusive access.

    • w+bx or wb+x create binary file for update with exclusive access.

    Opening a file with any of the exclusive modes above fails if the file already exists or cannot be created. Otherwise, the file is created with exclusive (non-shared) access. Additionally, a safer version of fopen() called fopen_s() is also available. That is what I would use in your code if I were you, but I'll leave that up for you to decide and change.

  • CLOCK_REALTIME represents the machine's best-guess as to the current wall-clock, time-of-day time. This means that CLOCK_REALTIME can jump forwards and backwards as the system time-of-day clock is changed, including by NTP.

    CLOCK_MONOTONIC represents the absolute elapsed wall-clock time since some arbitrary, fixed point in the past. It isn't affected by changes in the system time-of-day clock.

    If you want to compute the elapsed time between two events observed on the one machine without an intervening reboot, CLOCK_MONOTONIC is the best option.

\$\endgroup\$
2
  • \$\begingroup\$ -lrt isn't required iirc, it compiles fine on Ubuntu! Anyway, thanks :) Let me make the adjustments! I have updated the code on Git with even more modifications so that I can do both single threaded and multithreaded benchmark, can you take a look at that too maybe (link in Q)? \$\endgroup\$ Feb 26 '14 at 13:16
  • \$\begingroup\$ @noobprohacker Post it as another question later today and I'll take a look at it. \$\endgroup\$
    – syb0rg
    Feb 26 '14 at 18:30
5
\$\begingroup\$

For starters, don't use __inline__. It isn't portable. Instead, use the standard C inline.

Next, use more whitespace. Complex calls and almost all conditionals should go into multiple lines, while operations should be spaced. Also, explain what you are doing:

// original
snprintf(&(checksum[i*2]), 3, "%02x", (unsigned int)digest[i]);

// what it ought to look like
snprintf(                           // Describe
        &(checksum[i * 2]),         // what
        3,                          // you
        "%02x",                     // are
        (unsigned int)digest[i]     // doing
        );

and similarly, conditionals:

// original
    dd = (strcmp(argv[2], "--dumpdigits") == 0) ? 1 : 0; }

/* Invalid command line parameters */
else { fprintf(stderr, "Error: Invalid command-line arguments!\nUsage: pibench [digits] [parameter]\nParameter:\n--printdigits : Prints all digits on console\n--nodigits : Suppresses printing of digits on console\n--dumpdigits : Saves all the digits to a text file\n\nUsage example: pibench 50000 --printdigits\n"); exit(1); }



// what it ought to look like
    dd = (strcmp(argv[2], "--dumpdigits") == 0) ? 1 : 0;
} else {
    fputs (
            "Error: Invalid command-line arguments!\n"
            "Usage: pibench [digits] [parameter]\n"
            "Parameters:\n"
            "\t--printdigits : Prints all digits on console\n"
            "\t--nodigits : Suppresses printing of digits on console\n"
            "\t--dumpdigits : Saves all the digits to a text file\n\n"
            "Usage example: pibench 50000 --printdigits\n",
            stderr
            );

    return 1;
}

Now above I did a few more things:

  • Multiple strings on multiple lines will automatically get joined - use them.
  • Use hard tabs when explaining options.
  • Don't use printf() unless you need it. Use fputs() instead.
  • Don't use exit() unless you are within a function that must return void. Use return 1; instead.

Finally, use char **argv. It doesn't change anything and anyone worth their salt is aware of both, but at least to me, this seems cleaner:

// original
int main(int argc, char *argv[]) {

// what it should look like
int main (int argc, char **argv) {

Other than that, your code seems pretty clean. Well done!

\$\endgroup\$
1
  • \$\begingroup\$ Thanks :) Let me make the adjustments! I have updated the code on Git with even more modifications so that I can do both single threaded and multithreaded benchmark, can you take a look at that too maybe (link in Q)? \$\endgroup\$ Feb 26 '14 at 13:15
5
\$\begingroup\$

On top on haneefmubarak's comment, I'd like to point out a couple of things I do not quite like :

Variable names

I think this :

mpz_t v1, v2, v3, v4, v5;
mpf_t V1, V2, V3, total, tmp, res;

says it all. Not only the variables are meaningless names but on top of that, the case does matter. It's hard for everyone to read this without being confused.

Also, unsigned long how_many_digits = 10000; should probably be called something more descriptive like nb_of_digits or digits_number.

Variable declaration

You declare all the variable at the very beginning of the function. Not only this make the function even longer that it should be but on top of that it makes things hard to read without any added value.

One doesn't want to go back and forth dozens of lines in the code to see where this variable finally gets defined/used/declared if everything could be done in just a couple of lines away.

Also, as you try to define variables smaller to where it's useful, you'll notice that you are doing things which are not really useful : for instance, in unsigned long how_many_digits = 10000;, we never ever use the value 10000 but still, you have to write it, the compiler has to read it and even worth you and I have to read it.

Early return

Instead of doing

if (A) { foo(); } else { bar(); return; }  foobar()

you could just do:

if (!A) { bar(); return; } foo(); foobar()

to make things much more linear.

Automatic evaluation to true/false

You can use the fact that in C, zero evaluates to false and non-zero to true.

Here what it looks like after taking into account these simple comments :

/*
 *
 * Simple PI Benchmarking tool
 * Author: Suyash Srijan
 * Email: suyashsrijan@outlook.com
 *
 * This program calculates how much time your CPU takes to compute n digits of PI using Chudnovsky Algorithm
 * (http://en.wikipedia.org/wiki/Chudnovsky_algorithm) and uses the GNU Multiple Precision Arithmetic Library
 * for computation.
 *
 * For verification of digits, you can download the digits from here: http://piworld.calico.jp/estart.html
 *
 * It's a single threaded program but you can compile it with OpenMP support to enable parallelization.
 * WARN: OpenMP support is experimental
 *
 * Compile using gcc : gcc -O2 -Wall -o pibench pibench.c -lgmp -lssl -lcrypto
 * Compile using gcc (with OpenMP): gcc -O2 -Wall -o pibench pibench.c -lgmp -lssl -lcrypto -fopenmp
 *
 */

#include <gmp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sys/resource.h>
#include <sys/utsname.h>
#include <openssl/md5.h>

/* Import OpenMP header if compiling with -fopenmp */
#if defined(_OPENMP)
#include <omp.h>
#endif

/* You can't compile this on Windows */
#ifdef _WIN32
#error >>> Fatal: It is not possible to compile this program on Windows <<<
#endif

/* Build timestamp */
#define build_time __TIME__
#define build_date __DATE__

/* Calculate log to the base 2 using GCC's bit scan reverse intrinsic */
__inline__ unsigned int clc_log2(const unsigned int num) {
    return ((num <= 1) ? 0 : 32 - (__builtin_clz(num - 1)));
}

/* Calculate MD5 checksum for verification */
__inline__ char *clc_md5(const char *string) {
    MD5_CTX context;
    MD5_Init(&context);
    MD5_Update(&context, string, strlen(string));

    unsigned char digest[16];
    MD5_Final(digest, &context);

    char *checksum = (char*)malloc(33);
    for (int i = 0; i < 16; ++i) {
        snprintf(&(checksum[i*2]), 3, "%02x", (unsigned int)digest[i]);
    }
    return checksum;
}

/* Calculate pi digits main function */
__inline__ char *clc_pi(unsigned long dgts)
{
    double bits = clc_log2(10);
    unsigned long precision = (dgts * bits) + 1;
    mpf_set_default_prec(precision);
    mpz_t v1, v2, v3, v4, v5;
    mpf_t V1, V2, V3, total, tmp, res;
    mpz_inits(v1, v2, v3, v4, v5, NULL);
    mpf_inits(res, tmp, V1, V2, V3, total, NULL);
    mpf_set_ui(total, 0);
    mpf_sqrt_ui(tmp, 10005);
    mpf_mul_ui(tmp, tmp, 426880);

    /* Get high-res time */
    struct timespec start;
    clock_gettime(CLOCK_REALTIME, &start);

    /* Print total iterations and start computation of digits */
    unsigned long iters = (dgts / 15) + 1;
    printf("Total iterations: %lu\n\n", iters - 1);

#if defined(_OPENMP)
#pragma omp parallel for private(v1, v2, v3, v4, v5, V1, V2, V3, ti) reduction(+:total)
#endif

    /* Iterate and compute value using Chudnovsky Algorithm */
    for (unsigned long int i = 0x0; i < iters; i++) {
        unsigned long int ti = i * 3;
        unsigned long int constant1 = 545140134;
        unsigned long int constant2 = 13591409;
        unsigned long int constant3 = 640320;

        mpz_fac_ui(v1, 6 * i);
        mpz_set_ui(v2, constant1);
        mpz_mul_ui(v2, v2, i);
        mpz_add_ui(v2, v2, constant2);
        mpz_fac_ui(v3, ti);
        mpz_fac_ui(v4, i);
        mpz_pow_ui(v4, v4, 3);
        mpz_ui_pow_ui(v5, constant3, ti);
        if ((1 & ti) == 1) { mpz_neg(v5, v5); }
        mpz_mul(v1, v1, v2);
        mpf_set_z(V1, v1);
        mpz_mul(v3, v3, v4);
        mpz_mul(v3, v3, v5);
        mpf_set_z(V2, v3);
        mpf_div(V3, V1, V2);
        mpf_add(total, total, V3);

        /* Print interations executed if debugging (I don't like spamming stdout unnecesarily) */
#ifdef DEBUG
        printf("Iteration %lu of %lu successfully executed\n", i, iters - 1);
#endif
    }

    /* Some final computations */
    mpf_ui_div(total, 1, total);
    mpf_mul(total, total, tmp);

    /* Get high-res time */
    struct timespec end;
    clock_gettime(CLOCK_REALTIME, &end);

    /* Calculate and print time taken */
    double time_taken = (double)(end.tv_sec - start.tv_sec) + (double)(end.tv_nsec - start.tv_nsec) / 1E9;
    printf("Done!\n\nTime taken (seconds): %lf\n", time_taken);

    /* Store output */
    mp_exp_t exponent;
    char * oput = mpf_get_str(NULL, &exponent, 10, dgts, total);

    /* Free up space consumed by variables */
    mpz_clears(v1, v2, v3, v4, v5, NULL);
    mpf_clears(res, tmp, V1, V2, V3, total, NULL);

    /* Return value */
    return oput;
}

/* Entry point of program */
int main(int argc, char *argv[]) {
    /* Set number of threads if compiling with -fopenmp */
#if defined(_OPENMP)
    omp_set_num_threads(8);
#endif

    /* Try setting process priority to highest */
    if (setpriority(PRIO_PROCESS, (id_t)0, -20) == -1) {
        printf("WARN: Unable to max out priority. Did you not run this app as root?\n");
    }

    /* Parse command line */
    if (argc != 3 || (strcmp(argv[2], "--printdigits") && strcmp(argv[2], "--nodigits") && strcmp(argv[2], "--dumpdigits"))) {
        fprintf(stderr, "Error: Invalid command-line arguments!\nUsage: pibench [digits] [parameter]\nParameter:\n--printdigits : Prints all digits on console\n--nodigits : Suppresses printing of digits on console\n--dumpdigits : Saves all the digits to a text file\n\nUsage example: pibench 50000 --printdigits\n");
        return 1;
    }

    char *tmp_ptr;
    unsigned long nb_digits = strtol(argv[1], &tmp_ptr, base);
    int print_digits = strcmp(argv[2], "--printdigits");
    int dump_digits = strcmp(argv[2], "--dumpdigits");

    /* Invalid command line parameters */

    /* Print introductory text */
    struct utsname uname_ptr;
    uname(&uname_ptr);
    printf("\n---------------------------------------------------------------");
    printf("\nPi Bench v1.0 beta (%s)\nBuild date: %s %s\n", uname_ptr.machine, build_date, build_time);
    printf("---------------------------------------------------------------\n\n");

    /* Check if digits isnt zero or below */
    if (nb_digits < 1) { fprintf(stderr, "Error: Digit cannot be lower than 1\n"); exit(1); }

    /* Calculate digits of pi */
    printf("Computing %lu digits of PI...\n", nb_digits);
    char *digits_of_pi = clc_pi(nb_digits);

    /* Print the digits if user specified the --printdigits flag */
    if (print_digits) {
        printf("Here are the digits:\n\n%.1s.%s\n", digits_of_pi, digits_of_pi + 1);
    }

    /* Save digits to text file if user specified the --dumpdigits flag */
    if (dump_digits) {
        FILE *file;
        if ((file = fopen("pidigits.txt", "w"))) {
            fprintf(file, "%.1s.%s\n", digits_of_pi, digits_of_pi + 1);
            fclose(file);
        } else {
            fprintf(stderr, "Error while opening file\n");
            return -1;
        }
    }


    /* Print MD5 checksum */
    char *md5 = clc_md5(digits_of_pi);
    printf("MD5 checksum (for verification): %s\n", md5);

    /* Free the memory */
    free(digits_of_pi);

    /* Time to go! */
    printf("Goodbye!\n");
    return 0;
}
\$\endgroup\$
2
  • \$\begingroup\$ A possible solution to the variable v1, v2, vx, ... problem may be to simply use v[x+1] instead. \$\endgroup\$ Feb 18 '14 at 13:28
  • \$\begingroup\$ Thanks :) Let me make the adjustments! I have updated the code on Git with even more modifications so that I can do both single threaded and multithreaded benchmark, can you take a look at that too maybe? \$\endgroup\$ Feb 26 '14 at 13:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.